PureComponents vs Functional Components with hooks
Tags: webdev, tutorial, react, performance
Published: 2022-09-13 11:29:10


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9kpqrao8h63q56a4xoxr.png)

Originally published at [https://www.developerway.com](https://www.developerway.com). The website has more articles like this 😉

---

Do you agree that everything was better in the good old days? Cats were fluffier, unlimited chocolate didn’t cause diabetes and in React we didn’t have to worry about re-renders: `PureComponent` or `shouldComponentUpdate` would take care of them for us.

When I read comments or articles on React re-renders, the opinion that because of hooks and functional components we’re now in re-rendering hell keeps popping up here and there. This got me curious: I don’t remember the “good old days” being particularly good in that regard. Am I missing something? Is it true that functional components made things worse from re-renders perspective? Should we all migrate back to classes and `PureComponent`?

So here is another investigation for you: looking into `PureComponent` and the problem it solved, understanding whether it can be replaced now in the hooks & functional components world, and discovering an interesting (although a bit useless) quirk of React re-renders behavior that I bet you also didn’t know 😉

## PureComponent, shouldComponentUpdate: which problems do they solve?

First of all, let’s remember what exactly is `PureComponent` and why we needed `shouldComponentUpdate`.

### Unnecessary re-renders because of parents


As we know, today a parent's re-render is one of the reasons [why a component can re-render itself](https://www.developerway.com/posts/react-re-renders-guide#part2.2). If I change state in the `Parent` component it will case its re-render, and as a consequence, the `Child` component will re-render as well:

```tsx
const Child = () => <div>render something here</div>;

const Parent = () => {
  const [counter, setCounter] = useState(1);

  return (
    <>
      <button onClick={() => setCounter(counter + 1)}>Click me</button>
      <!-- child will re-render when "counter" changes-->
      <Child />
    </>
  )
}
```

It’s exactly the same behavior as it was before, with class-based components: state change in `Parent` will trigger re-render of the `Child`:

```tsx
class Child extends React.Component {
  render() {
    return <div>render something here</div>
  }
}

class Parent extends React.Component {
  super() {
    this.state = { counter: 1 }
  }

  render() {
    return <>
      <button onClick={() => this.setState({ counter: this.state.counter + 1 })}>Click me</button>
      <!-- child will re-render when state here changes -->
      <Child />
    </>
  }
}
```

[See example in codesandbox](https://codesandbox.io/s/parent-child-re-renders-ipbe3l?file=/src/App.tsx)

And again, exactly the same story as today: [too many or too heavy re-renders](https://www.developerway.com/posts/how-to-write-performant-react-code) in the app could cause performance problems.

In order to prevent that, React allows us to override `shouldComponentUpdate` method for the class components. This method is triggered before the component is supposed to re-render. If it returns `true`, the component proceeds with its lifecycle and re-renders; if `false` - it won’t. So if we wanted to prevent our `Child` components from parent-induced re-renders, all we needed to do is to return `false` in `shouldComponentUpdate`:

```tsx
class Child extends React.Component {
  shouldComponentUpdate() {
    // now child component won't ever re-render
    return false;
  }

  render() {
    return <div>render something here</div>
  }
}
```

But what if we want to pass some props to the `Child` component? We actually need this component to update itself (i.e. re-render) when they change. To solve this, `shouldComponentUpdate` gives you access to `nextProps` as an argument and you have access to the previous props via `this.props`:

```tsx
class Child extends React.Component {
  shouldComponentUpdate(nextProps) {
    // now if "someprop" changes, the component will re-render
    if (nextProps.someprop !== this.props.someprop) return true;

    // and won't re-render if anything else changes
    return false;
  }

  render() {
    return <div>{this.props.someprop}</div>
  }
}
```

Now, _if and only if_ `someprop` changes, `Child` component will re-render itself.

Even if we add some state to it 😉. Interestingly enough, `shouldComponentUpdate` is called before state updates as well. So this method is actually very dangerous: if not used carefully, it could cause the component to misbehave and not update itself properly on its state change. Like this:

```tsx
class Child extends React.Component {
  constructor(props) {
    super(props);
    this.state = { somestate: 'nothing' }
  }

  shouldComponentUpdate(nextProps) {
    // re-render component if and only if "someprop" changes
    if (nextProps.someprop !== this.props.someprop) return true;
    return false;
  }

  render() {
    return (
      <div>
        <!-- click on a button should update state -->
        <!-- but it won't re-render because of shouldComponentUpdate -->
        <button onClick={() => this.setState({ somestate: 'updated' })}>Click me</button>
        {this.state.somestate}
        {this.props.someprop}
      </div>
    )
  }
}
```


In addition to props, `Child` component has some state now, which is supposed to be updated on button click. But clicking on this button won’t cause `Child` component to re-render, since it’s not included in the `shouldComponentUpdate`, so the user will actually never see the updated state on the screen.

[See example in codesandbox](https://codesandbox.io/s/should-component-update-broken-ytvzu0?file=/src/App.tsx)

In order to fix it, we also need to add state comparison to the `shouldComponentUpdate` function: React sends us `nextState` there as the second argument:

```tsx
shouldComponentUpdate(nextProps, nextState) {
    // re-render component if "someprop" changes
    if (nextProps.someprop !== this.props.someprop) return true;

    // re-render component if "somestate" changes
    if (nextState.somestate !== this.state.somestate) return true;
    return false;
  }
```

As you can imagine, writing that manually for every state and prop is a recipe for a disaster. So most of the time it would be something like this instead:

```tsx
shouldComponentUpdate(nextProps, nextState) {
    // re-render component if any of the prop change
    if (!isEqual(nextProps, this.prop)) return true;

    // re-render component if "somestate" changes
    if (!isEqual(nextState, this.state)) return true;

    return false;
  }
```

And since this is such a common use case, React gives us `PureComponent` in addition to just `Component`, where this comparison logic is implemented already. So if we wanted to prevent our `Child` component from unnecessary re-renders, we could just extend `PureComponent` without writing any additional logic:

```tsx
// extend PureComponent rather than normal Component
// now child component won't re-render unnecessary
class PureChild extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = { somestate: 'nothing' }
  }

  render() {
    return (
      <div>
        <button onClick={() => this.setState({ somestate: 'updated' })}>Click me</button>
        {this.state.somestate}
        {this.props.someprop}
      </div>
    )
  }
}
```

Now, if we use that component in the `Parent` from above, it will not re-render if the parent’s state changes, and the Child’s state will work as expected:

```tsx
class Parent extends React.Component {
  super() {
    this.state = { counter: 1 }
  }

  render() {
    return <>
      <button onClick={() => this.setState({ counter: this.state.counter + 1 })}>Click me</button>
      <!-- child will NOT re-render when state here changes -->
      <PureChild someprop="something" />
    </>
  }
}
```

[See example in codesandbox](https://codesandbox.io/s/pure-components-example-072li4?file=/src/App.tsx)

### Unnecessary re-renders because of state


As mentioned above, `shouldComponentUpdate` provides us with both props AND state. This is because it is triggered before _every_ re-render of a component: regardless of whether it’s coming from parents or its own state. Even worst: it will be triggered on _every_ call of `this.setState`, regardless of whether the actual state changed or not.

```tsx
class Parent extends React.Component {
  super() {
    this.state = { counter: 1 }
  }

  render() {
    <!-- every click of the button will cause this component to re-render -->
    <!-- even though actual state doesn't change -->
    return <>
      <button onClick={() => this.setState({ counter: 1 })}>Click me</button>
    </>
  }
}
```

[See example in codesandbox](https://codesandbox.io/s/just-component-with-state-emhncm?file=/src/App.tsx)

Extend this component from `React.PureComponent` and see how re-renders are not triggered anymore on every button click.

Because of this behavior, every second recommendation on “how to write state in React” from the good old days mentions “set state only when actually necessary” and this is why we should explicitly check whether state has changed in `shouldComponentUpdate` and why `PureComponent` already implements it for us.

Without those, it is actually possible to cause performance problems because of unnecessary state updates!

---

To summarise this first part: `PureComponent` or `shouldComponentUpdate` were used to prevent performance problems caused by unnecessary re-renders of components caused by their state updates or their parents re-renders.

## PureComponent/shouldComponentUpdate vs functional components & hooks

And now back to the future (i.e. today). How do state and parent-related updates behave now?

### Unnecessary re-renders because of parents: React.memo


As we know, [re-renders from parents](https://www.developerway.com/posts/react-re-renders-guide#part2.2) are still happening, and they behave in exactly the same way as in the classes world: if a parent re-renders, its child will re-render as well. Only in functional components we don’t have neither `shouldComponentUpdate` nor `PureComponent` to battle those.

Instead, we have `React.memo`: it’s a higher-order component supplied by React. It behaves almost exactly the same as `PureComponent` when it comes to props: even if a parent re-renders, re-render of a child component wrapped in React.memo won’t happen unless its props change.

If we wanted to re-implement our `Child` component from above as a functional component with the performance optimization that `PureComponent` provides, we’d do it like this:

```tsx
const Child = ({ someprop }) => {
  const [something, setSomething] = useState('nothing');

  render() {
    return (
      <div>
        <button onClick={() => setSomething('updated')}>Click me</button>
        {somestate}
        {someprop}
      </div>
    )
  }
}

// Wrapping Child in React.memo - almost the same as extending PureComponent
export const PureChild = React.memo(Child);
```

And then when `Parent` component changes its state, `PureChild` won’t re-render: exactly the same as a `PureChild` based on `PureComponent`:

```tsx
const Parent = () => {
  const [counter, setCounter] = useState(1);

  return (
    <>
      <button onClick={() => setCounter(counter + 1)}>Click me</button>
      <!-- won't re-render because of counter change -->
      <PureChild someprop="123" />
    </>
  )
}
```

[See example in codesandbox](https://codesandbox.io/s/child-in-memo-v6r22i?file=/src/App.tsx:682-686)

### Props with functions: React.memo comparison function

Now let’s assume `PureChild` accepts `onClick` callback as well as a primitive prop. What will happen if I just pass it like an arrow function?

```tsx
<PureChild someprop="123" onClick={() => doSomething()} />
```

**Both** `React.memo` and `PureComponent` implementation will be broken: `onClick` is a function (non-primitive value), on every `Parent` re-render it will be re-created, which means on every `Parent` re-render `PureChild` will think that `onClick` prop has changed and will re-render as well. Performance optimization is gone for both.

And here is where functional components have an advantage.

`PureChild` on `PureComponent` can’t do anything about the situation: it would be either up to the parent to pass the function properly, or we would have to ditch `PureComponent` and re-implement props and state comparison manually with `shouldComponentUpdate`, with `onClick` being excluded from the comparison.

With `React.memo` it’s easier: we can just pass to it the comparison function as a second argument:

```tsx
// exclude onClick from comparison
const areEqual = (prevProps, nextProps) => prevProps.someprop === nextProps.someprop;

export const PureChild = React.memo(Child, areEqual);
```

Essentially `React.memo` combines both `PureComponent` and `shouldComponentUpdate` in itself when it comes to props. Pretty convenient!

[See example in codesandbox](https://codesandbox.io/s/child-in-memo-with-comparison-44yhbe?file=/src/App.tsx)

Another convenience: we don’t need to worry about state anymore, as we’d do with `shouldComponentUpdate`. `React.memo` and its comparison function only deals with props, Child’s state will be unaffected.

### Props with functions: memoization

While comparison functions from above are fun and look good on paper, to be honest, I wouldn’t use it in a real-world app. (And I wouldn’t use `shouldComponentUpdate` either). Especially if I’m not the only developer on the team. It’s just too easy to screw it up and add a prop without updating those functions, which can lead to such easy-to-miss and impossible-to-understand bugs, that you can say goodbye to your karma and the sanity of the poor fella who has to fix it.

And this is where actually `PureComponent` takes the lead in convenience competition. What we would do in the good old days instead of creating inline functions? Well, we’d just bind the callback to the class instance:

```tsx
class Parent extends React.Component {
  onChildClick = () => {
    // do something here
  }

  render() {
    return <PureChild someprop="something" onClick={this.onChildClick} />
  }
}
```


This callback will be created only once, will stay the same during all re-renders of `Parent` regardless of any state changes, and won’t destroy PureComponent’s shallow props comparison.

In functional components we don’t have class instance anymore, everything is just a function now, so we can’t attach anything to it. Instead, we have… 
nothing
… a few ways to preserve the reference to the callback, depending on your use case and how severe are the performance consequences of Child’s unnecessary re-renders.

#### 1. useCallback hook

The simplest way that will be enough for probably 99% of use cases is just to use [useCallback](https://reactjs.org/docs/hooks-reference.html#usecallback) hook. Wrapping our `onClick` function in it will preserve it between re-renders if dependencies of the hook don’t change:

```tsx
const Parent = () => {
  const onChildClick = () => {
    // do something here
  }

  // dependencies array is empty, so onChildClickMemo won't change during Parent re-renders
  const onChildClickMemo = useCallback(onChildClick, []);

  return <PureChild someprop="something" onClick={onChildClickMemo} />
}
```

What if the `onClick` callback needs access to some Parent’s state? In class-based components that was easy: we had access to the entire state in callbacks (if we bind them properly):

```tsx
class Parent extends React.Component {
  onChildClick = () => {
    // check that count is not too big before updating it
    if (this.state.counter > 100) return;
    // do something
  }

  render() {
    return <PureChild someprop="something" onClick={this.onChildClick} />
  }
}
```

In functional components it’s also easy: we just add that state to the dependencies of `useCallback` hook:

```tsx
const Parent = () => {
  const onChildClick = () => {
    if (counter > 100) return;
    // do something
  }

  // depends on somestate now, function reference will change when state change
  const onChildClickMemo = useCallback(onChildClick, [counter]);

  return <PureChild someprop="something" onClick={onChildClickMemo} />
}
```

With a small caveat: `useCallback` now depends on `counter` state, so it will return a different function when the counter changes. This means `PureChild` will re-render, even though it doesn’t depend on that state explicitly. Technically - unnecessary re-render. Does it matter? In most cases, it won’t make a difference, and performance will be fine. Always measure the actual impact before proceeding to further optimizations.

In **very** rare cases when it actually matters (measure first!), you have at least two more options to work around that limitation.

#### 2. setState function


If all that you do in the callback is setting state based on some conditions, you can just use the pattern called [“updater function”](https://beta.reactjs.org/apis/react/useState#updating-state-based-on-the-previous-state) and move the condition inside that function.

Basically, if you’re doing something like this:

```tsx
const onChildClick = () => {
  // check "counter" state
  if (counter > 100) return;
  // change "counter" state - the same state as above
  setCounter(counter + 1);
}
```

You can do this instead:

```tsx
const onChildClick = () => {
  // don't depend on state anymore, checking the condition inside
  setCounter((counter) => {
    // return the same counter - no state updates
    if (counter > 100) return counter;

    // actually updating the counter
    return counter + 1;
  });
}
```

That way `onChildClick` doesn’t depend on the counter state itself and state dependency in the `useCallback` hook won’t be needed.

[See example in codesandbox](https://codesandbox.io/s/memo-with-callbacks-kjzo9y?file=/src/App.tsx)

#### 3. mirror state to ref

In case you _absolutely_ have to have access to different states in your callback, and _absolutely_ have to make sure that this callback doesn’t trigger re-renders of the `PureChild` component, you can “mirror” whichever state you need to a ref object.

[Ref object](https://beta.reactjs.org/apis/react/useRef) is just a mutable object that is preserved between re-renders: pretty much like state, but:

* it’s mutable
* it doesn’t trigger re-renders when updated

You can use it to store values that are not used in render function (see [the docs](https://beta.reactjs.org/apis/react/useRef#referencing-a-value-with-a-ref) for more details), so in case of our callbacks it will be something like this:

```tsx
const Parent = () => {
  const [counter, setCounter] = useState(1);
  // creating a ref that will store our "mirrored" counter
  const mirrorStateRef = useRef(null);

  useEffect(() => {
    // updating ref value when the counter changes
    mirrorStateRef.current = counter;
  }, [counter])

  const onChildClick = () => {
    // accessing needed value through ref, not statej - only in callback! never during render!
    if (mirrorStateRef.current > 100) return;
    // do something here
  }

  // doesn't depend on state anymore, so the function will be preserved through the entire lifecycle
  const onChildClickMemo = useCallback(onChildClick, []);

  return <PureChild someprop="something" onClick={onChildClickMemo} />
}
```

First: creating a ref object. Then in useEffect hook updating that object with the state value: ref is mutable, so it’s okay, and its update won’t trigger re-render, so it’s safe. Lastly, using the ref value to access data in the callback, that you’d normally access directly via state. And tada 🎉: you have access to state value in your memoized callback without actually depending on it.

[See example in codesandbox](https://codesandbox.io/s/mirror-state-in-ref-13i71j?file=/src/App.tsx)

Full disclaimer: I have **never** needed this trick in production apps. It’s more of a thought exercise. If you find yourself in a situation where you’re actually using this trick to fix actual performance problems, then chances are something is wrong with your app architecture and there are easier ways to solve those problems. Take a looks at [Preventing re-renders with composition](https://www.developerway.com/posts/react-re-renders-guide#part3) part of [React re-renders guide](https://www.developerway.com/posts/react-re-renders-guide), maybe you can use those patterns instead.

### Props with arrays and objects: memoization


Props that accept arrays or objects are equally tricky for `PureComponent` and `React.memo` components. Passing them directly will ruin performance gains since they will be re-created on every re-render:

```tsx
<!-- will re-render on every parent re-render -->
<PureChild someArray={[1,2,3]} />
```

And the way to deal with them is exactly the same in both worlds: you either pass state directly to them, so that reference to the array is preserved between re-renders. Or use any memoization techniques to prevent their re-creation. In the old days, those would be dealt with via external libraries like `memoize`. Today we can still use them, or we can use [useMemo](https://reactjs.org/docs/hooks-reference.html#usememo) hook that React gives us:

```tsx
// memoize the value
const someArray = useMemo(() => ([1,2,3]), [])
<!-- now it won't re-render -->
<PureChild someArray={someArray} />
```

### Unnecessary re-renders because of state

And the final piece of the puzzle. Other than parent re-renders, `PureComponent` prevents unnecessary re-renders from state updates for us. Now that we don’t have it, how do we prevent those?

And yet another point to functional components: we don’t have to think about it anymore! In functional components, state updates that don’t actually change state don’t trigger re-render. This code will be completely safe and won’t need any workarounds from re-renders perspective:

```tsx
const Parent = () => {
  const [state, setState] = useState(0);

  return (
    <>
      <!-- we don't actually change state after setting it to 1 when we click on the button -->
      <!-- but it's okay, there won't be any unnecessary re-renders-->
      <button onClick={() => setState(1)}>Click me</button>
    </>
  )
}
```

This behavior is called [“bailing out from state updates”](https://reactjs.org/docs/hooks-reference.html#bailing-out-of-a-state-update) and is supported natively in useState hook.

## Bonus: bailing out from state updates quirk

Fun fact: if you don’t believe me and react docs in the example above, decide to verify how it works by yourself and place `console.log` in the render function, the result will break your brain:

```tsx
const Parent = () => {
  const [state, setState] = useState(0);

  console.log('Log parent re-renders');

  return (
    <>
      <button onClick={() => setState(1)}>Click me</button>
    </>
  )
}
```

You’ll see that the first click on the button `console.log` is triggered: which is expected, we change state from 0 to 1. But the second click, where we change state from 1 to 1, which is supposed to bail, will also trigger `console.log`! But third and all the following clicks will do nothing… 🤯 WTF?

Turns out this is a feature, not a bug: React is being smartass here and tries to make sure that it’s actually safe to bail out on the first “safe” state update. The “bailing out” in this context means that children won’t re-render, and `useEffect` hooks won’t be triggered. But React will still trigger Parent’s render function the first time, just in case. See this issue for more details and rationale: [useState not bailing out when state does not change · Issue #14994 · facebook/react](https://github.com/facebook/react/issues/14994)

[See example in codesandbox](https://codesandbox.io/s/bailing-out-state-updates-vwvp8e?file=/src/App.tsx)

## TL;DR Summary

That is all for today, hope you had fun comparing the past and the future, and learned something useful in the process. Quick bullet points of the above wall of text:

* when migrating PureComponent to functional components, wrapping component in React.memo will give you the same behavior from re-renders perspective as PureComponent
* complicated props comparison logic from shouldComponentUpdate can be re-written as an updater function in React.memo
* no need to worry about unnecessary state updates in functional components - React handles them for us
* when using “pure” components in functional components, passing functions as props can be tricky if they need access to state since we don’t have instance anymore. But we can use instead:
  - useCallback hook
  - updater function in state setter
  - “mirror” necessary state data in ref
* arrays and objects as props of “pure” components need to be memoized both for PureComponent and React.memo components

Live long and prosper in re-renders-free world! ✌🏼

---

Originally published at [https://www.developerway.com](https://www.developerway.com). The website has more articles like this 😉

[Subscribe to the newsletter](https://www.developerway.com), [connect on LinkedIn](https://www.linkedin.com/in/adevnadia/) or [follow on Twitter](https://twitter.com/adevnadia) to get notified as soon as the next article comes out.