Reputation: 5332
Can useMemo
be used just to avoid extra referential equality checking code/vars when setting state during a render?
Example: useMemo
with a setState
during render taken from this rare documented use case:
function ScrollView({row}) {
let [isScrolling, setIsScrolling] = useState(false);
const lessCodeThanCheckingPrevRow = useMemo(
() => {
// Row changed since last render. Update isScrolling.
setIsScrolling(true); // let's assume the simplest case where prevState isn't needed here
},
[row]
);
return `Scrolling down: ${isScrolling}`;
}
Above drastically reduces code and extra vars, only otherwise used for equality checks, so why do the docs imply referential equality checks should be done manually?
Upvotes: 18
Views: 35134
Reputation: 243
setXState calls should not be made during render, and this includes inside the body of a useMemo. I couldn't find a stated exception for setXState calls that only update the component being rendered (a la getDerivedStateFromProps), but there might be.
At minimum, want to make sure no one reads this as 'setState calls in useMemo are safe'.
React now considers this an error, and throws a somewhat opaque error message about it. One of many discussions on the error message here states this issue will begin failing harder in future versions of React. This github issue seems to state that all setState calls made during rendering are errors.
const useSomeHook = (props: {x, setX}) => {
const [y, setY] = useState(0);
props.setX(1); // wrong
const [a, setA] = useState(() => {
props.setX(2); // wrong
return 0;
);
const z = useMemo(() => {
props.setX(3); // wrong
return 0;
}
if (y < 4) {
setY(4); // maybe allowed? Since only updates current component
}
return <div>{y + props.x}</div>;
}
On React:18.2.0 I spent a while trying to find the source of this error, and discovered it was because my code boiled down to calling something that updated state within a useMemo. Whether or not the error was thrown was highly dependent on the code path, and minor tweaks would hide it, without solving it.
Upvotes: 3
Reputation: 1297
if the problem is having a value that changes on every user action which in this case is scrolling, then useMemo is not the best way to go. Perhaps useCallback is a better option. The code block would be the same as you have defined it above, but instead of useMemo, useCallback, and include the state in the dependency array.
Upvotes: 1
Reputation: 16354
This seems to be an elegant way to reduce boiler plate to me. I created a codesandbox to validate its behaviour.
const UnitUnderTest = ({prop}) => {
let [someState, setSomeState] = useState(false);
const lessCodeThanCheckingPrevRow = useMemo(
() => setSomeState(current => !current),
[prop],
);
useEffect(() => console.log('update finished'), [prop])
console.log('run component');
return `State: ${someState}`;
}
const App = () => {
const [prop, setProp] = useState(false);
const handleClick = () => setProp(current => !current);
return (
<div>
<button onClick={handleClick} >change prop</button>
<UnitUnderTest prop={prop} />
</div>
)
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Output when clicking the button to change the prop passed to the component:
> run component
> run component
> update finished
As you can see the component has been run twice before the update cycle completed. This is equivalent to the the behaviour of getDerivedStateFromProps
.
I guess that there is no deeper thought behind why the docs propose a slightly different technique. In a way this is a manual check too but in a neat way. +1 for the idea.
Upvotes: 8
Reputation: 469
Use the useEffect hook for this behavior. useMemo is used to store a value that might not necessarily change over each renders, so that you avoid useless re-calculation of that value
Upvotes: 4