1

The goal - I want to set some local form state based on interaction with three checkboxes (see below).

Check box options

The rule - If two options are selected then the channel state should be set to 'multi' if one option is selected then the channel state should be the value of the checkbox that is selected (eg eml, push, inapp).

The problem: I am able to successfully update the channel state in my local form state. But, when I lift the state up via props.onGenerateResult(data), I find that the channel state in the data object is an empty string.

Heres what I have so far...

State to check status of checkboxes - Initially they are all set to false

const [channel, setChannel] = useState('');
const [channelOptions, setChannelOptions] = useState({
    eml: false,
    push: false,
    inapp: false
});

Handler that updates channelOptions state using computed property values

const channelSelectionChangeHandler = (e) => {
    setChannelOptions((prevState) => {
        return {
            ...prevState,
            [e.target.value]: e.target.checked
        };
    });
};

JSX pointing to channelSelectionChangeHandler() to run

<Checkbox value="eml" onChange={channelSelectionChangeHandler}>Email</Checkbox>
<Checkbox value="push" onChange={channelSelectionChangeHandler}>Push</Checkbox>
<Checkbox value="inapp" onChange={channelSelectionChangeHandler}>Inapp</Checkbox>

Submit handler that fires on form submission - Lifts all my form state up (This includes channel) and where I handle conditional checking to set the channel state.

const onSubmitFormHandler = (e) => {
    e.preventDefault();

    // set channel state here
    for (const props in channelOptions) {
        if (channelOptions[props] === true) {
            selectedChannels.push(props);
        }
    }

    if (selectedChannels.legnth === 1) {
        setChannel(selectedChannels[0]);
    } else {
        setChannel('multi');
    }


    const data = { name, date, channel, content, target, type, frequency, market, city,zone };

    props.onGenerateResult(data);
};

Why is this? How should I best approach this so that my channel state also gets lifted up? This could be simple if I used select with multiple option but I prefer to use input type=checkboxes. Additionally, I'm thinking of moving the for in loop and if check into my channelSelectionChangeHandler(). Just keep the submission handler lean.

Thanks in advance and hope this all made sense.

Samuel
  • 5,529
  • 5
  • 25
  • 39
  • Duplicate: [Why calling react setState method doesn't mutate the state immediately?](https://stackoverflow.com/questions/30782948/why-calling-react-setstate-method-doesnt-mutate-the-state-immediately) –  Jul 13 '21 at 13:11
  • @ChrisG This using React useState hook... – Samuel Jul 13 '21 at 13:25
  • ? What do you mean? That's it's not a dupe because you're using functional components? Lol, no. Anyway, the quick solution is to simply set a variable like `newChannel` instead, then use that variable twice: `setChannel(newChannel)` and `const data = { ..., channel: newChannel, ...}` –  Jul 13 '21 at 13:31
  • why do you need this as a state at all? `const data = { ..., channel = selectedChannels.length > 1 ? "multi" : "", ... }` – Thomas Jul 13 '21 at 20:15
  • @Thomas selected channel is ab oject. – Samuel Jul 14 '21 at 06:21
  • @Samuel in your example, `channel` is a string, either `"multi"` or `""`. `channelOptions` is an object, and `selectedChannels` is an array; which has a `length` property and the value for `channel` seems to be derived directly from this length. if `length > 1 ? "multi" : ""`. And I don't see any other place where you use the `channel` state, except to build `props.onGenerateResult(data);`. So again, do you need `channel` as a state? – Thomas Jul 16 '21 at 19:04

1 Answers1

1

The comment from @chris-g is correct. Even though your example is using the useState hook, the behavior is similar to setState: The value of channel doesn't change immediately after you call setChannel. Instead, react will call your component again and the return value of useState will contain the updated value. But this is to late for your callback.

To fix your issue, I would recommend two changes:

  1. Since channel is only computed based on channelOptions, use useMemo for channel instead of useState, :
const [channelOptions, setChannelOptions] = useState({
    eml: false,
    push: false,
    inapp: false
});
const channel = useMemo(() => {
  // Compute the value of channel based on the current value of channelOptions
  return Object.keys(channelOptions).reduce((channel = '', key) => {
    if (channelOptions[key] === true) { 
      return channel === '' ? key : 'multi'
    }
    return channel;
  }, '');
// add channelOptions as dependency so this value gets recomputed when they change
}, [channelOptions])

  1. Use useCallback for onSubmitFormHandler and use the new channel value returned by useMemo:
const onSubmitFormHandler = useCallback((e) => {
    e.preventDefault();

    const data = { name, date, channel, content, target, type, frequency, market, city,zone };

    props.onGenerateResult(data);
}, [channel, name, date, content, target, type, frequency, market, city, zone, props.onGenerateResult]);

  • Thanks for this solution! I'm pretty new to React and haven't come across `useMemo` and `useCallback` in my learning yet. But this works really well. My only issue now is when I reset the form I cannot reset the values of the checkboxes back to false. I guess this is because I dont have specific state for the checkboxes? – Samuel Jul 13 '21 at 16:37
  • How are you resetting it? And did you assign your state correctly to the checkboxes? In your code example, it looks like you are setting `value` of each `Checkbox` to a string instead of the actual state value. Maybe try `Email`? – Maximilian Ochs Jul 14 '21 at 07:32
  • I have the following `
    `. This will trigger a form reset and state reset whenever the following is clicked - `
    – Samuel Jul 14 '21 at 12:38
  • 1
    Resolved the issue. I have marked your solution as the best one. Thank for your help!! – Samuel Jul 14 '21 at 21:26