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>
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.
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 theuseEffect
oruseCallback
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.
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.