Sharcoux
Sharcoux

Reputation: 6075

React rerenders my memoïzed component when I use an HOC in the parent

I am having an issue with an Image blinking because it is rendered for no reason, despite using React.memo, and despite non of it's props or state being changed.

I succeeded here to make the correct use of React.memo to make this work, BUUUT, for a reason that I don't understand, if I use an High Order Component within the Parent component, memo doesn't work anymore and I get my blinking issue again.

Here is a snack that illustrates the problem.

Here is the code:

import * as React from 'react';
import { Text, View, StyleSheet } from 'react-native';

let interval = null

const Icon = ({ name }) => {
  // We emulate a rerender of the Icon by logging 'rerender' in the console
  console.log('rerender')
  return <Text>{name}</Text>
}

const Memo = React.memo(Icon, () => true)

const withHOC = (Comp) => (props) => {
  return <Comp {...props}/>
}

export default function App() {
  const [state, setState] = React.useState(0)
  const name = 'constant'
  // Change the state every second
  React.useEffect(() => {
    interval = setInterval(() => setState(s => s+1), 1000)
    return () => clearInterval(interval)
  }, [])
  // Remove this line and replace NewView by View to see the expected behaviour
  const NewView = withHOC(View)
  return (
    <NewView>
      <Memo name={name} />
    </NewView>
  );
}

I don't understand why my HOC breaks the memoization, and I have no idea how to prevent the blinking in my app and still be able to use HOC...

Upvotes: 1

Views: 1899

Answers (2)

Rico Kahler
Rico Kahler

Reputation: 19202

You're re-creating the HOC within your render function. Because of this React can't keep any of that component's children consistent between renders.

If you move the HOC creation outside of the render, then it'll work!

const Text = 'span';
const View = 'div';

let interval = null

const Icon = ({ name }) => {
  // We emulate a rerender of the Icon by logging 'rerender' in the console
  console.log('rerender')
  return <Text>{name}</Text>
}

const Memo = React.memo(Icon, () => true)

const withHOC = (Comp) => (props) => {
  return <Comp {...props}/>
}

// move it out here!
// 👇👇👇
const NewView = withHOC(View)
// 👆👆👆

function App() {
  const [state, setState] = React.useState(0)
  const name = 'constant'
  // Change the state every second
  React.useEffect(() => {
    interval = setInterval(() => setState(s => s+1), 1000)
    return () => clearInterval(interval)
  }, [])
  // Remove this line and replace NewView by View to see the expected behaviour
  
  return (
    <NewView>
      <Memo name={name} />
    </NewView>
  );
}
ReactDOM.render(<App />, document.querySelector('#root'));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root"></div>


Edit: I saw your comment in the other answer.

Ok, but how can I use the HOC within my component? Because I need to provide the props and the state to the hoc...

If you do need to create the HOC within the component, you can wrap it with useMemo and that will also work because React will preserve your HOC reference between renders if the dependencies of the useMemo don't change (note: this will not work if your hook dependencies change for every render).

function App() {
  // ...
  const NewView = useMemo(() => withHOC(View), []);
}

Although this works, it can be kind of wonky. In general, hooks and HOCs are not patterns to be used together. The React core team created hooks to replace HOCs. Before you continue down that road, I would attempt to see if you can write your HOC as a hook. I think you'll find that it's a lot more natural.

Upvotes: 5

Michalis Garganourakis
Michalis Garganourakis

Reputation: 2930

On every re-render you create a new NewView so the old one (along with your Icon) will be destroyed for the new one. So, it wasn't actually a re-render that was happening on the Icon, but a totally new render of a new Icon.

If you move const NewView = withHOC(View) outside your App function, your HOC will be called once, creating a NewView that will be used on every re-render and this will prevent your Icon from also being destroyed and as you have it memoized, you are safe from unnecessary re-renders.

import * as React from 'react';
import { Text, View, StyleSheet } from 'react-native';

let interval = null

const Icon = ({ name }) => {
  // We emulate a rerender of the Icon by logging 'rerender' in the console
  console.log('rerender')
  return <Text>{name}</Text>
}

const Memo = React.memo(Icon, () => true)

const withHOC = (Comp) => (props) => {
  return <Comp {...props}/>
}

const NewView = withHOC(View);

export default function App() {
  const [state, setState] = React.useState(0)
  const name = 'constant'
  // Change the state every second
  React.useEffect(() => {
    interval = setInterval(() => setState(s => s+1), 1000)
    return () => clearInterval(interval)
  }, [])
  // Remove this line and replace NewView by View to see the expected behaviour
  return (
    <NewView>
      <Memo name={name} />
    </NewView>
  );
}

To better understand what's happening, I added a log here on your Icon component so you can see that the component unmounts on every parent re-render, while it's forced to be destroyed by the creation of a totally new NewView with a new memoized Icon.

Upvotes: 2

Related Questions