Search code examples
javascriptreactjsreact-nativereact-hooksuse-effect

Conditionally sending an object of key-value (array) from child to parent component


I am trying to pass an object of key-value pairs with the value being an array of strings from a Child component to a Parent's state. These values will come from UI-Kitten's multi-select component and it'll be passed as an object to the Parent.

I understand how passing of data works between parent and child components and the usage of useEffect in a component for componentDidMount or any conditional fires of an effect. But this case is rather complex.

I could possibly dump the entire Child component into the Parent as I have tried replicating something similar and it works i.e. I get the object of key-value pairs updated whenever user selects/deselects an option from the multi-select. Hoping that I could take the Child-Parent so the Parent wouldn't clutter up as the Child component is pretty long.

I hope I could get some help with it. Thank you!

Parent Component (the state, technologyExperience, is not updating):

  const [technologyExperience, setTechnologyExperience] = React.useState({
    game: [],
    web: [],
    mobile: [],
    database: [],
    machineLearning: []
  });

  const getSelections = data => {
    setTechnologyExperience(prevData => {
      return { ...prevData, ...data };
    });

    // Only prints on first load
    console.log('parent.getSelections', technologyExperience);
  };

  React.useEffect(() => {
    // Only prints on first load
    console.log('parent.useEffect.getSelections', technologyExperience);
  }, [getSelections]);


// calling of the Child component
<InputBackgroundSelect getSelections={getSelections} />

Child component:

const InputBackgroundSelect = props => {
  // All the values needed to pass to Parent
  const [gameIndex, setGameIndex] = React.useState([]);
  const displayGameDev = gameIndex.map(index => {
    return gameDevData[index.row];
  });

  const [webIndex, setWebIndex] = React.useState([]);
  const displayWebDev = webIndex.map(index => {
    return webDevData[index.row];
  });

  const [mobileIndex, setMobileIndex] = React.useState([]);
  const displaymobileDev = mobileIndex.map(index => {
    return mobileDevData[index.row];
  });

  const [dbIndex, setDBIndex] = React.useState([]);
  const displayDb = dbIndex.map(index => {
    return dbData[index.row];
  });

  const [mlIndex, setMLIndex] = React.useState([]);
  const displayMl = mlIndex.map(index => {
    return mlData[index.row];
  });

  // As for the conditional effect, I have tried using
  // [displayGameDev, displayWebDev, displaymobileDev, displayDb, displayMl] too.
  React.useEffect(() => {
    // Only prints on first load but not when selecting/deselecting options
    console.log('Calling props.getSelections'); 

    props.getSelections({
      game: displayGameDev,
      web: displayWebDev,
      mobile: displaymobileDev,
      database: displayDb,
      machineLearning: displayMl
    });
  }, [setGameIndex, setWebIndex, setMobileIndex, setDBIndex, setMLIndex]);

// An example of the 5 repeated Selects
      <Select
        label='Mobile Development'
        style={styles.selectInput}
        multiSelect={true}
        selectedIndex={mobileIndex}
        onSelect={index => setMobileIndex(index)} // Fires when user selects an option from the Select
        placeholder='Select'
        value={displaymobileDev.join(', ')}
      >
        {mobileDevData.map((value, key) => (
          <SelectItem key={key} title={value} />
        ))}
      </Select>

enter image description here


Solution

  • Issue

    It seems the bulk of your issue here is a misunderstanding of React hook dependencies. You are using the state updater function as the dependency, but these are stable references, i.e. they are the same from render to render, so if included in a hook's dependency array they won't trigger the hook callback.

    useState

    Note

    React guarantees that setState function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.

    You will see the useEffect callback called once on the initial render, but since the dependencies are stable they are never updated and will never trigger the effect callback again later.

    Additional Issues

    Within the InputBackgroundSelect component the displayGameDev, displayWebDev, displaymobileDev, displayDb, displayMl, getSelections variables are dependencies for the useEffect for the getSelections callback, but they are declared in the function body of the component. This means they are redeclared each render cycle, thus triggering some render looping.

    Solution

    Fix the useEffect dependencies in both the parent and child component. Hook dependencies are basically anything that is referenced within the callback that make change from render to render.

    Parent

    React.useEffect(() => {
      console.log('parent.useEffect.getSelections', technologyExperience);
    }, [technologyExperience]);
    

    Child

    React.useEffect(() => {
      getSelections({
        game: displayGameDev,
        web: displayWebDev,
        mobile: displaymobileDev,
        database: displayDb,
        machineLearning: displayMl
      });
    }, [displayGameDev, displayWebDev, displaymobileDev, displayDb, displayMl, getSelections]);
    

    Additional note about React state updates, they are asynchronous, so you can't console log your state right after enqueueing an update, it'll still only be the state value from the current render cycle, not what you just enqueued for the next render.

    const getSelections = data => {
      setTechnologyExperience(prevData => {
        return { ...prevData, ...data };
      });
    
      // This will be current state, not next state
      console.log('parent.getSelections', technologyExperience);
    };
    

    Just use the useEffect with dependency on technologyExperience` to log when it updates. (i.e. the issue fixed above 😀)

    To fix the render looping you need to memoize both the getSelections callback with useCallback.

    const getSelections = React.useCallback(data => {
      setTechnologyExperience(prevData => ({ ...prevData, ...data }));
    }, []);
    

    And the displayGameDev, displayWebDev, displaymobileDev, displayDb, displayMl listed here with useMemo.

    const [gameIndex, setGameIndex] = React.useState([]);
    const displayGameDev = React.useMemo(
      () => gameIndex.map((index) => gameDevData[index.row]),
      [gameIndex]
    );
    
    const [webIndex, setWebIndex] = React.useState([]);
    const displayWebDev = React.useMemo(
      () => webIndex.map((index) => webDevData[index.row]),
      [webIndex]
    );
    
    const [mobileIndex, setMobileIndex] = React.useState([]);
    const displaymobileDev = React.useMemo(
      () => mobileIndex.map((index) => mobileDevData[index.row]),
      [mobileIndex]
    );
    
    const [dbIndex, setDBIndex] = React.useState([]);
    const displayDb = React.useMemo(
      () => dbIndex.map((index) => dbData[index.row]),
      [dbIndex]
    );
    
    const [mlIndex, setMLIndex] = React.useState([]);
    const displayMl = React.useMemo(
      () => mlIndex.map((index) => mlData[index.row]),
      [mlIndex]
    );
    

    Here's a link to a forked Expo Snack.