Skip to content

React – Missing data in lifted state

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.

Answer

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]);