fbpx

Should you start optimizing every part of your React app?

When we talk about app optimizations and performance it’s important to note it comes with a cost. Recently I’ve been working on a project where the useMemo or useCallback hooks were overused and I’ll try to explain why this is not the best practice.

While working with React you will encounter terms like PureComponent, memo, useMemo and useCallback.

What they have in common is prevention of unnecessary rerendering of a component.

What is PureComponent?

PureComponent is the Component which handles shouldComponentUpdate lifecycle method by itself. PureComponent does a shallow comparison on both props and state of the component on every change and decides whether to re-render it or not.

One thing you should keep in mind – never mutate the object because if you mutate an object in the parent component your pure child component won’t update.

//Bad
<SomeItem = {() => this.calucateSomething(id)} />

Another thing you should avoid is binding functions in a render. We should pass only the reference to SomeItem, because every time render triggers, a new function with the new reference is created.

//Good
<SomeItem = {this.calucateSomething} />

If your child component renders the same props/state you can use PureComponent and its usage is straightforward.

class Player extends React.PureComponent {
  //.....
}

What is React.memo

React memo is a higher order component that is similar to PureComponent but only for functional components.

If your component renders the same result given the same props then it will not re-render. If the component also has useState or useContext hooks, it will still re-render on state/context changes. As you can see from the example below every time we click a button to increase a count a Player components re-renders. We know that our Player component doesn’t depend on count state so we can use a React.memo to prevent renders. You want to use React.memo when the component depends only on props.

The more often the component renders with the same props, the heavier and the more computationally expensive the output is, the more chances are that component needs to be wrapped in React.memo()

import React from "react";
import Player from "./Player";
​
class MemoPerfomance extends React.Component {
  state = {
    count: 0,
    player: { name: "Player", surname: "Number 2" }
  };
  setCounterHandler = () => {
    const { count } = this.state;
    this.setState({ count: count + 1 });
  };
​
  render() {
    const { count, player } = this.state;
    return (
      <div className="App">
        <h1>Hello memo</h1>
        <button onClick={this.setCounterHandler}>Counter</button>
        <h2>{count}</h2>
        <Player player={player} />
      </div>
    );
  }
}
export default MemoPerfomance;
​
import React from "react";
const Player = ({ player}) => {
  console.log("RENDER")
  return (
    <div>
      <p>
        {player.name} {player.surname}
      </p>
      <p>I’m player</p>
    </div>
  );
};
​
export default React.memo(Player);

In terms of performance we should do it like this because we are always pointing to the same object in memory:

<Player player={player}  />

If you pass props this way the new object will be created on every single render. That means even though we memoized our Player component, it is still going to re-render just because a new object is created:

<Player player={{name:"Player" surname:"Number 2"}}  />

useCallback and useMemo

useCallback wraps a function and memoizes it, so if none of the dependencies change we will get back a cached or memoized version of the function.

useCallback returns a memoized callback. It takes two arguments, a callback and an array of dependencies. The second argument is mandatory with the useCallback hook. If we don’t have dependencies the second argument should just be an empty array. If that is not the case, you need to pass every dependency to the array because you will get an error in the console even though it might still work (it will not render when a missed dependency changes).

useMemo is very similar to the useCallback hook. The only difference is that you actually use it when you want to cache the value of a function that does something computationally expensive.

import React, { useState, useCallback, useMemo } from "react";
​
const CountStats = React.memo(({ onClick, count }) => {
  console.log("RENDER");
  return <button onClick={onClick}>{count}</button>;
});
​
const DualStatsCounter = () => {
  const [goals, setGoals] = useState(0);
  const [assists, setAssists] = useState(0);
​
  const setGoalsHandler = () => setGoals(goals + 1);
  const setAssistHandler = () => setAssists(assists + 1);
​
  const incrementGoals = useCallback(setGoalsHandler, [goals]);
  const incrementAssists = useCallback(setAssistHandler, [assists]);
​
  const makeShowPlayerValue = useMemo(() => {
    return goals * 7.3 * 1000 + " Euro";
  }, [goals]);
​
  return (
    <>
      <div>
        Goals
        <CountStats count={goals} onClick={incrementGoals} />
      </div>
      <div>
        Assists
        <CountStats count={assists} onClick={incrementAssists} />
      </div>
      Value: {makeShowPlayerValue}
    </>
  );
};
​
​
export default DualStatsCounter;

As you can see from this example. If we don’t put a useCallback on incrementGoals and incrementAssist, these functions will be recreated every time we click on it.

Conclusion

Be careful not to spend too much time on optimization because it won’t always be worth it. You should optimize the part of the code that is doing something complex and changes props/state frequently. My suggestion is that you start to optimize when you detect regressions in performance. Then you can easily test your application in the real-world and get the best feedback. In the end, just remember: React is fast, your code might not be!