I know there already are already some related questions, like How can React useEffect watch and update state?, but still, I don't get it totally.
Let's say I set an index
state based on a prop; and I need to sanitize that value anytime it is set.
<MyComponent index={4}/>
This is how I attempted to do it:
useEffect(() => {
setIndex(props.index);
}, [props.index]);
useEffect(() => {
const sanitized = sanitizeIndex(index);
setIndex(sanitized);
},[index])
const sanitizeIndex = index => {
//check that index exists in array...
//use fallback if not...
//etc.
return index
}
It does not work (infinite loop), since the state is watched and updated by the second useEffect()
.
Of course, I could avoid this by calling sanitizeIndex()
on the prop, so I only need a single instance of useEffect()
:
useEffect(() => {
setIndex(sanitizeIndex(props.index));
}, [props.index]);
Thing is, I call setIndex
plenty of times in my code, and I fear to miss using sanitizeIndex
.
Is there another way to "catch" and update a state value being set ?
Thanks !
This seems like a good case for a custom hook. Here's an example of how to implement one for your case (given the information currently provided in your question), including comments about how/why:
Be sure to read the documentation for
useCallback
if you are not already familiar with it. It's especially important to understand how to use the dependency array (link 1, link 2) when using hooks which utilize it (likeuseCallback
anduseEffect
).
<div id="root"></div><script src="https://unpkg.com/react@17.0.2/umd/react.development.js"></script><script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js"></script><script src="https://unpkg.com/@babel/standalone@7.16.12/babel.min.js"></script>
<script type="text/babel" data-type="module" data-presets="env,react">
const {useCallback, useEffect, useState} = React;
/**
* You didn't show exactly how you are sanitizing, so I'm using this function
* instead. It will simply make sure the input number is always even by
* adding 1 to it if it's odd.
*/
function makeEven (n) {
return n % 2 === 0 ? n : n + 1;
}
function useSanitizedIndex (sanitizeIndex, unsanitizedIndex) {
const [index, setIndex] = useState(sanitizeIndex(unsanitizedIndex));
// Like setIndex, but also sanitizes
const setSanitizedIndex = useCallback(
(unsanitizedIndex) => setIndex(sanitizeIndex(unsanitizedIndex)),
[sanitizeIndex, setIndex],
);
// Update state if arguments change
useEffect(
() => setSanitizedIndex(unsanitizedIndex),
[setSanitizedIndex, unsanitizedIndex],
);
return [index, setSanitizedIndex];
}
function IndexComponent (props) {
// useCallback memoizes the function so that it's not recreated on every
// render. This also prevents the custom hook from looping infinintely
const sanitizeIndex = useCallback((unsanitizedIndex) => {
// Whatever you actually do to sanitize the index goes in here,
// but I'll just use the makeEven function for this example
return makeEven(unsanitizedIndex);
// If you use other variables in this function which are defined in this
// component (e.g. you mentioned an array state of some kind), you'll need
// to include them in the dependency array below:
}, []);
// Now simply use the sanitized index where you need it,
// and the setter will sanitize for you when setting it (like in the
// click handler in the button below)
const [index, setSanitizedIndex] = useSanitizedIndex(sanitizeIndex, props.index);
return (
<div>
<div>Sanitized index (will always be even): {index}</div>
<button onClick={() => setSanitizedIndex(5)}>Set to 5</button>
</div>
);
}
function Example () {
const [count, setCount] = useState(0);
return (
<div>
<div>Count: {count}</div>
<button onClick={() => setCount(n => n + 1)}>Increment</button>
<IndexComponent index={count} />
</div>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
</script>