Reputation: 6075
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
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
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