Table of Contents
Before you optimize the amount of re-renders in React application, try to profile and find if there is a problem.
If you profiled and see the problem with heavy re-renders which affect performance, then you might try to optimize it.
Next, we'll see in which cases React components re-render and how to avoid it.
When components re-render?
Look at the example below
const Child = () => <p>some text</p>
const Parent = () => {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(c => c + 1)}>
{count}
</button>
<Child />
</>
)
}
Will Child
re-render on the button click? Spoiler: yes
React re-renders all the component's tree by default if the state or props changes. That's why in the example above Child
will re-render.
The thing is that Child
's re-render is not a problem as long as the render is lightweight. Often it is better for performance to re-render, than use optimizations like useMemo
, useCallback
, React.memo
, etc.
useCallback
and useMemo
Don't use these hooks by default. It is crucial to get used to the fact that it is a bad practice to put useCallback
and useMemo
hooks everywhere.
useCallback
and useMemo
are just functions with their own abstractions. On every render, these functions are called, and a callback that is passed as an argument is created again.
However, if child components are not wrapped in React.memo
, there will be no effect and everything will re-render.
You can use useCallback
or useMemo
in these cases:
- Child components are wrapped in memo and you don't want them to re-render
- You have heavy calculations that you don't want to recalculate on each render
React.memo
This hoc is useful when you have a deep component tree or some heavy calculations on render
const Component = (props) => {
return (
// deep tree of components
)
}
export const MemoizedComponent = React.memo(Component)
Component wrapped in memo would re-render only if props, internal state, or context will change.
It is important to remember that React.memo
uses shallow equal
of props by default. That's why you should memoize non-primitive values like objects, arrays, functions, etc. in the parent component.
Move state down
Let's imagine you have this component:
const Parent = () => {
const [count, setCount] = useState(0)
return (
<>
<ComponentA onChange={setCount} count={count} />
<ComponentB count={count} />
<ComponentC />
</>
)
}
Component A and component B are using count
state, but component C doesn't need it. If we let it be as it is, component C will re-render each time the count
changes if it is not wrapped in React.memo
.
We can move count
state down by creating another component like this:
const Union = () => {
const [count, setCount] = useState(0)
return (
<>
<ComponentA onChange={setCount} count={count} />
<ComponentB count={count} />
</>
)
}
const Parent = () => {
return (
<>
<Union />
<ComponentC />
</>
)
}
Now component C does not depend on count
state and will not re-render when the count changes.
Move content up
Let's complicate the previous example
const Parent = () => {
const [count, setCount] = useState(0)
return (
<ComponentA onChange={setCount} count={count}>
<ComponentB count={count} />
<ComponentC />
</ComponentA>
)
}
Now component A wraps all the content and we can't move the state down. In that case, we can move component A and component B up.
const Union = ({ children }) => {
const [count, setCount] = useState(0)
return (
<ComponentA onChange={setCount} count={count}>
<ComponentB count={count} />
{children}
</ComponentA>
)
}
After we moved component A, component B, and their state up, we can pass component C as a child in Union
component.
const Parent = () => {
return (
<Union>
<ComponentC />
</Union>
)
}
Now again, if count
state changes, component C will not re-render.
HOC for loading
Sometimes we want to handle some loading state inside a component.
For example:
const Parent = ({ isLoading }) => {
const someCb = useCallback(...)
const someCalculatedValue = useMemo(...)
useEffect(...)
if (isLoading) {
return <Loader />
}
return ...
}
Here we see a lot of hooks, that we don't need while data is loading. It is redundant to create a callback, calculate values, or fire effects while isLoading
is false.
If you try to move isLoading
condition upwards, when isLoading
changes from true to false, the number of hooks on render will change, and React will fire an error.
To avoid errors and redundant hooks we can move this condition in HOC.
const WithLoading = (Component) => {
return ({ isLoading, ...props }) => {
if (isLoading) {
return <Loader />
}
return <Component {...props} />
}
}
Then we can use WithLoading
HOC like this.
const Parent = () => { ... }
export const WithLoadingParent = WithLoading(Parent)
Uncontrolled input
Quite often you can see controlled input in React apps. Many developers use this pattern by default even if there is no reason to use it.
If you don't need to validate input value or run any other logic on the fly, you can just use native inputs.
const Parent = () => {
const inputRef = useRef()
const handleSearch = () => {
console.log(inputRef.current.value)
}
return (
<>
<input ref={inputRef} type="text" />
<button onClick={handleSearch}>Search</button>
</>
)
}
Thereby component will not re-render when the input's value changes, but we can easily get the value when we need it.
Wrap up
The methods described above are only applicable if there are problems with heavy re-renders that you want to avoid.
It is very important to remember that optimizations are often not free. You should profile first and only then try to solve the problems.
Top comments (5)
Strong disagree.
useCallback
is always a good idea and should always be used. Not usinguseCallback
is just a shortcut that people take, because they don't want to bother providing a dependency array, because they do not fully understand how dependency arrays work and thats the #1 reason why React apps break down. Anything that you provide as props to a component, should always be memoized in some way. If you don't memoize the props that you provide to a component, it's going to ALWAYS rerender, You basically give up any control over how/when a component rerenders. It may be easy to understand why a component is rerendering if your app is only 1 level deep. But when there's a tree of 50 components that all rerender because at the top level, you have one unmemiozed prop, it becomes an absolute nightmare to debug. Props should only change when you actually intend for them to change.HoCs are out of fashion, for good reasons and you wont see them used anymore in most well known npm packages for React. You also won't see documentation on HoCs in the new React beta documentation that they're working on. Your example does not explain why you want to use 3 hooks to achieve the same functionality as your HoC... Infact it's not clear what you're trying to achieve with that HoC in the first place.
Do you use
React.memo
in every component? If not, there will be re-render anywaysTry this example in codesandbox and you'll see that Child will rerender on the button click, so
useMemo
doesn't do anything in that case.Thats not what useMemo is for. useMemo is for calculating derived state. But you are right yes, if the parent rerenders, the children will rerender, unless you create a memoized component. The problem with not memoizing props that you pass to components is that it changes the internal state of that component on every rerender, which can result in further rerenders due to side effects and/or cause unexpected behavior inside child components because they cannot rely on the props ever being referentially equal. For example, all your side effects and memoized derived state will always trigger/change on every render.
Ask yourself this: do you think the people at Facebook got bored and decided to add a completely useless useCallback hook to react?
I'm not saying that useCallback is useless. My point is that it shouldn't be used everywhere, you should understand and know why you want use it. Dependant effects in child component is a good case, when you could use useCallback.
The point is, you dont know what happens in the child component. If the parent needs to know what goes on in the child, you do not have seperation of concerns, which is a bad practice. Therefor if you pass anything to the props of a child component, you should always make sure it is memoized, so the child can do with those props whatever it wants, because it can rely on that very important contract between the parent and the child, that the props will ONLY change if the value is actually different. This is the responsibility of the parent component. If you cannot rely on this, you need to keep a mental model of your entire app in your head at all times and if you forget anything while fixing a problem in a child component, you will be confronted with debugging nightmare that gets progressively worse, the further down the tree you are.