DEV Community

vithano for Workiz

Posted on • Edited on

Optimizations in React part 2

To useMemo or not to useMemo?

You might have noticed that our CoolButton doesn't properly render the (+) sign:

<CoolButton clickHandler={increment}>+</CoolButton>
Enter fullscreen mode Exit fullscreen mode

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>;
  });
Enter fullscreen mode Exit fullscreen mode

Instead of nothing let's render the children:

return <button onClick={handler}>{children}</button>;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)
)}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 and useCallback can be used interchangeably(From React docs)

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
shvmbisht profile image
Shivam bisht

Amazing content

Collapse
 
vithano profile image
vithano

Thank you!

Collapse
 
markrity profile image
Mark Davydov

Thats great Vitali