It’s another one of the React Dos and Don'ts which you have found a lot on the internet. But give me a chance and keep reading. I’ve been developing React web applications for about 8 years. For the last 3 years I’ve been working on optimizing React apps to make them fast with good FPS. Let’s get started.
The React team has created really good and detailed documentation. I like it much more than the previous one. My favorite part is about the Render and Commit phases. Please read it if you haven’t. The most common mistake that I’ve seen during code reviews was triggering a redundant commit phase. It’s important to batch state updates and avoid triggering setState
synchronously in effect.
Don’t
const ModalWithState = ({ isVisible }) => {
const [isModalContentVisible, setIsModalContentVisible] = useState(isVisible);
useEffect(() => {
setIsModalContentVisible(isVisible);
}, [isVisible]);
return (
<Modal
isModalContentVisible={isModalContentVisible}
onShow={() => {
setIsModalContentVisible(true);
}}
onClose={() => {
setIsModalContentVisible(false);
}}
>
<div>Modal content</div>
</Modal>
);
};
Do
const ModalWithState = ({ isVisible }) => {
const [isModalContentVisible, setIsModalContentVisible] = useState(isVisible);
const [prevIsVisible, setIsPrevIsVisible] = useState(isVisible);
if (isVisible !== prevIsVisible) {
setIsModalContentVisible(isVisible);
setIsPrevIsVisible(isVisible);
}
return (
<Modal
isModalContentVisible={isModalContentVisible}
onShow={() => {
setIsModalContentVisible(true);
}}
onClose={() => {
setIsModalContentVisible(false);
}}
>
<div>Modal content</div>
</Modal>
);
};
There are some exceptions when it’s necessary to update state synchronously. E.g. when handling different client and server content.
And keep in mind - “Always check whether you can reset all state with a key or calculate everything during rendering instead”.
The next part is a long discussion about when to use useCallback/useMemo and when not to.
There are two opposite approaches:
- Use it only when you’ve identified performance bottlenecks. There is a good article about this called - When to useMemo and useCallback.
- You can “put down a pillow” and use it everywhere. I have run tests and found that it has a negative impact on performance only with high numbers of useMemo/useCallback usage on the page (from ~2000 on weaker devices).
I recommend striking a happy medium.
If you never benefit from that, don’s use it:
1.1. Don’t useuseCallback
when you pass event handlers as props in the base components (like button, div, etc.)
1.2. Don’t useuseMemo
for primitive memoization.
1.3. Free memoization is always the best one to use.If you definitely benefit from that now or in the future:
2.1. ApplyuseCallback
if you pass event handlers in the memoized components.
2.2. Don’t pass new functions or objects via context for every render unintentionally.
2.3. UseuseCallback/useMemo
when you don’t want to trigger effects for every render.
2.4. UseuseCallback
for non-base components.
1.1. Don’t use useCallback
when you pass event handlers as props in the base components (like button, div, etc.)
In the example below <button>
is a base component. If you use css-in-js
please keep in mind that the styled
button is also actually the base component.
Don’t
const Parent = ({ value }) => {
const [v, setV] = useState(0);
const onClick = useCallback(() => setV(v => v + 1), []);
return <button onClick={onClick}>Click me</button>;
}
Do
const Parent = ({ value }) => {
const [v, setV] = useState(0);
return <button onClick={() => setV(v => v + 1)} />;
}
1.2. Don’t use useMemo
for primitive memoization.
A huge number of useMemo
usage (2K - 100K, depending on device) could increase render time by 50%. I believe that ~2K useMemo
is a rare case for production applications, but why do you use it if there is no sense.
Don’t
const MyComponent = ({ value }) => {
const text = useMemo(() => `My value is ${value}`, [value]);
return <div>{text}</div>;
}
Do
const MyComponent = ({ value }) => {
const text = `My value is ${value}`;
return <div>{text}</div>;
}
1.3. Free memoization is always the best one to use.
I hope you have a good habit of moving initializations of constants outside of the render. In a very artificial example with a huge amount of re-renders there is a render degradation up to 60%.
Don’t
const MyComponent = () => {
const list = useMemo(() => ['one', 'two', 'three', 'four'], []);
return <ChildMemo list={list} />;
}
Do
const LIST = ['one', 'two', 'three', 'four'];
const MyComponent = () => {
return <ChildMemo list={LIST} />;
}
The cases below if you need it now or it could be applicable in the future.
2.1. Apply useCallback
if you pass event handlers in the memoized components.
Don’t
const ChildMemo = memo(function Child() {
return <button onClick={onClick}>Click me</button>;
});
const Parent = () => {
const [v, setV] = useState(0);
return <ChildMemo onClick={() => setV(v => v + 1)} />;
}
Do
const Parent = () => {
const [v, setV] = useState(0);
const handleOnClick = useCallback(() => setV(v => v + 1), []);
return <ChildMemo onClick={handleOnClick} />;
}
2.2. Don’t pass new functions or objects via context for every render unintentionally.
It avoids redundant re-renders of context subscribed components. You can find details in the documentation - Optimizing re-renders when passing objects and functions. I recommend enabling the eslint
rule regarding that - react/jsx-no-constructed-context-values.
Don’t
return (
<SomeContext.Provider value={{ foo: 'bar' }}>
...
</SomeContext.Provider>
)
Do
const foo = useMemo(() => ({ foo: 'bar' }), []);
return (
<SomeContext.Provider value={foo}>
...
</SomeContext.Provider>
)
2.3. Always use useCallback
if you don’t want to trigger effects for every render.
Also it gets rid of the disabling eslint
rule // eslint-disable-next-line react-hooks/exhaustive-deps
.
Don’t
const Child = ({ handleData, data }) => {
useEffect(() => {
myFetch().then((res) => {
handleData(res);
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return <div>{data}</div>
};
const Parent = () => {
const [data, setData] = useState(null);
return <Child handleData={(data) => setData(data)} data={data} />;
}
Do
const Child = ({ handleData, data }) => {
useEffect(() => {
myFetch().then((res) => {
handleData(res);
})
}, [handleData])
return <div>{data}</div>
};
const Parent = () => {
const [data, setData] = useState(null);
const handleData = useCallback((data) => setData(data), []);
return <Child handleData={handleData} data={data} />;
}
2.4. Call useCallback
for non base components.
Because in the future these components can be transformed to memoised components and such event handlers can break memoization.
Don’t
const Parent = ({ value }) => {
const [v, setV] = useState(0);
return <MyButton onClick={() => setV(v => v + 1)}>Click me</button>;
}
Do
const Parent = ({ value }) => {
const [v, setV] = useState(0);
const handleOnClick = useCallback(() => setV(v => v + 1), []);
return <MyButton onClick={handleOnClick} />;
}
Don’t dynamically create a new component on every render.
React will throw away the previous DOM subtree and create it from scratch instead of re-calculating the difference.
It negatively affects performance and can lead unpredictable behaviour by unmounting and mounting such components on every re-render:
- All effects will be triggered.
- The state will not be saved.
- Memoization will be broken.
Don’t
const Parent = ({ value }) => {
const Child = () => <div>{value}</div>;
return <Child />;
}
Do
const Child = ({ value }) => <div>{value}</div>;
const Parent = ({ value }) => {
return <Child value={value} />;
}
Don’t break memoization.
When you use memo
don’t pass children as props. In most cases it triggers re-render of memoized components for every parents’ re-renders. An exception is if children is a primitive data type (string, number, etc.).
Don’t
const Child = () => {
return <div>child</div>;
};
const MemoComponent = memo(function MemoComponent({ children }) {
// some heavy render
return <div>{children}</div>;
});
const Parent = () => {
return (
<MemoComponent>
<Child />
</MemoComponent>
);
};
Do1 (better than Do2)
const Child = () => {
return <div>child</div>;
};
const MemoComponent = memo(function MemoComponent() {
// some heavy render
return (
<div>
<Child />
</div>
);
});
const Parent = () => {
return <MemoComponent />;
};
Do2
const Child = () => {
return <div>child</div>;
};
const MemoComponent = memo(function MemoComponent({ children }) {
// some heavy render
return <div>{children}</div>;
});
const Parent = () => {
const memoChild = useMemo(() => <Child />, [])
return (
<MemoComponent>
{memoChild}
</MemoComponent>
);
};
useReducer
is often a better option.
Sometimes developers prefer to use a lot of useState
instead of one useReducer
. And I failed to find the valid reasons for that. Since we have automatic batching in React 18 this leads to less performance bottlenecks but it has still been a bad option. In the very artificial example with 11 useState
instead of 1 useReducer
there is a 45% re-render time growth.
Don’t
const MyComponent = ({ id }) => {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(false);
useEffect(() => {
setIsLoading(true);
myFetch(id).then((data) => {
setData(data);
setError(null);
}).catch((e) => {
setError(e);
}).finally(() => {
setIsLoading(false);
})
}, [id])
if (isLoading) {
return 'Loading';
}
if (error) {
return 'Error';
}
return <div>{data}</div>;
}
Do
const reducer = (state, action) => {
switch (action.type) {
case 'set data': {
return {
...state,
isLoading: false,
error: null,
data: action.payload,
}
}
case 'set isLoading': {
return {
...state,
isLoading: action.payload,
}
}
case 'set error': {
return {
...state,
error: action.payload,
}
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
};
const MyComponent = ({ id }) => {
const [{ data, isLoading, error }, dispatch] = useReducer(reducer, {
data: null,
isLoading: true,
error: null,
});
useEffect(() => {
dispatch({ type: 'set isLoading', payload: true });
myFetch(id).then((data) => {
dispatch({ type: 'set data', payload: data });
}).catch((e) => {
dispatch({ type: 'set error', payload: e });
})
}, [id])
if (isLoading) {
return 'Loading';
}
if (error) {
return 'Error';
}
return <div>{data}</div>;
}
useState
lazy initialization helps you avoid expensive calculations on every render.
There is documentation.
Don’t
const MyComponent = (props) => {
const [value, setValue] = useState(calculateSomethingExpensive(props));
return <div>{value}</button>;
}
Do
const MyComponent = (props) => {
const [value, setValue] = useState(() => calculateSomethingExpensive(props));
return <div>{value}</button>;
}
Keep you render pure.
Otherwise it could lead to different bugs such as unpredictable ref
values.
Don’t
const MyComponent = (props) => {
const ref = useRef(props);
ref.current = props;
return <div>text</div>;
}
Do
const MyComponent = (props) => {
const ref = useRef(props);
useEffect(() => {
ref.current = props;
})
return <div>text</div>;
}
I hope you’ve found something useful for yourself. Happy coding!
Top comments (0)