To useMemo or not to useMemo?
You might have noticed that our CoolButton doesn't properly render the (+) sign:
<CoolButton clickHandler={increment}>+</CoolButton>
When inserting anything inside a JsxElement like CoolButton, we don't present it to the user, but pass it as a prop called children.
const CoolButton = React.memo(({ clickHandler,children }) => {
const handler = () => {
ReallyImportantCalculation();
clickHandler();
};
return <button onClick={handler}></button>;
});
Instead of nothing let's render the children:
return <button onClick={handler}>{children}</button>;
Just as before, let's add some complexity to our design.
Instead of presenting the user with the (+) sign, let's create a "Clicker" game, which will consist of a button that changes its appearance based on the number of times we click on it.
We can start by passing an <img/>
instead of a (+) sign to our CoolButton:
<CoolButton clickHandler={increment}>
<img/>
</CoolButton>
When clicking the button we notice that our memoization was lost once again; re-rendering the button on every click..
Let us remember that in JSX <img/>
is not an html tag, it's actually a shorthand for React.createElement('img',props, ...children)
Turning our code into:
{createElement(CoolButton,{clickHandler:increment},
createElement('img',null, null)
)}
Now it's easy to see the exact problem: running createElement on every render creates a new child that's being passed to our CoolButton as a prop.
First we need to take out the creation of our child from inside our CoolButton:
const CurrentImage = <img/>;
<CoolButton clickHandler={increment}>
{CurrentImage}
</CoolButton>
You might be tempted to put the CurrentImage outside of our Counter, which would work, but seeing as CurrentImage will have a state based on our Counter we should use a different way:
const CurrentImage = useCallback(<img/>,[]);
<CoolButton clickHandler={increment}>
{CurrentImage}
</CoolButton>
Just like before, useCallback to the rescue!
Though it looks a bit weird, as our CurrentImage is not really a callback, but a value we want to memoize.
useMemo
useMemo, just like useCallback, takes a function that memoizes something and a dependency array that re-runs that function only when the dependencies change, in our case we want to memoize a JsxElement.
As we said earlier, the Children prop that we pass to our CoolButton changes on every render because we create a new CurrentImage every time.
We can useMemo to memoize CurrentImage and prevent the re-renders:
const CurrentImage = useMemo(() => <img/>,[]);
<CoolButton clickHandler={increment}>
{CurrentImage}
</CoolButton>
To make this a bit more interesting, let's add a new state called phaseImgUrl which will tell us which image we should be presenting for every phase of our Clicker:
const [phaseImgUrl, setPhaseImgUrl] = useState('');
const CurrentImage = useMemo(() => <img src={phaseImgUrl}/>,[phaseImgUrl]);
<CoolButton clickHandler={increment}>
{CurrentImage}
</CoolButton>
Here's some extra logic that will handle changing the phases when it reaches a certain threshold:
const phases = [
"https://media4.giphy.com...phase1",
"https://media4.giphy.com...phase2",
"https://media4.giphy.com...phase3",
];
useEffect(() => {
if (count != null) {
const phaseThreshold = 30;
const numPhases = phases.length;
const nextPhaseImgUrl =
phases[parseInt(count / phaseThreshold, 10) % numPhases];
if (nextPhaseImgUrl !== phaseImgUrl) {
setPhaseImgUrl(nextPhaseImgUrl);
}
}
}, [count]);
First we check if the count is valid, and then it's important to make sure that the phase is different from the last one, so that we don't cause extra setStates and re-renders.
And there we go!
In the next part we will talk about the added effects and the dangers within them.
Top comments (3)
Amazing content
Thank you!
Thats great Vitali