- expensive_operation
- unstable_reference
- bad_selector
- context
- memo
- requires_package
- requires_import
- Tags: expensive_operation
useState can accept a callback and execute it internally to get initial state.
If getUsers
only needs to run once, then wrap it in a callback to prevent it from running on every render.
- const users = useState(getUsers());
+ const users = useState(() => getUsers());
- Tags: unstable_reference, memo, requires_import
If a parent component rerenders, its children will also rerender.
If the children don't need to rerender, then you can wrap with memo
to prevent unnecessary rerenders.
+ import { memo } from 'react';
function Parent({ /* ... */ foo }) {
// ...
return <Child foo={foo} />;
}
- function Child({ foo }) {
+ const Child = memo(({ foo }) => {
// ...
}
- Tags: unstable_reference, memo, requires_import
A common mistake is to pass a function as a prop to a child component. Internally, React will check equivalence by reference. If the parent component rerenders, the callback will be a new value, rerendering unnecessarily.
Pass the callback as a prop by reference, and call it in the child component.
function Parent({ id }) {
- return <Child onClick={() => someFunction(id)} />;
+ return <Child id={id} someFunction={someFunction} />;
}
- function Child({ onClick }) {
- return <button onClick={onClick}></button>;
+ function Child({ id, someFunction }) {
+ return <button onClick={() => someFunction(id)}></button>;
}
Another solution is to wrap the callback in useCallback
to memoize it.
+ import { useCallback } from 'react';
function Parent({ id }) {
+ const onClick = useCallback(() => someFunction(id), [someFunction, id]);
- return <Child onClick={onClick} />;
}
function Child({ onClick }) {
return <button onClick={onClick}></button>;
}
- Tags: unstable_reference, context, requires_import, requires_package
In React, when a context value is changed, all components that useContext will re-render.
We solve this by using useContextSelector from the use-context-selector library.
+ import { useContextSelector } from 'use-context-selector';
- const { mode, setMode } = useContext(DarkModeContext);
+ const mode = useContextSelector(DarkModeContext, (context) => context.mode);
+ const setMode = useContextSelector(
+ DarkModeContext,
+ (context) => context.setMode
+ );
- Tags: expensive_operation, requires_import, requires_package
Every time the user types, the component re-renders.
If app is on React 18, use startTransition and usememo to concurrently handle expensive renders.
+ import { startTransition, useMemo } from 'react';
function NotesList({ notes }) {
- const [filter, setFilter] = useState('');
+ const [filterInput, setFilterInput] = useState('');
+ const [filterValue, setFilterValue] = useState('');
+ const filteredNotes = useMemo(() => {
+ return Object.values(notes)
+ .sort((a, b) => b.date.getTime() - a.date.getTime())
+ .filter(({ text }) => {
+ if (!filterValue) {
+ return true;
+ }
+
+ return text.toLowerCase().includes(filterValue.toLowerCase());
+ })
+ }, [notes, filterValue]);
return (
<>
<FilterInput
filter={filter}
onChange={() => {
+ setFilterInput(value);
+ startTransition(() => {
- setFilter(filterInput);
+ setFilterValue(value);
+ });
}}
noteCount={Object.keys(notes).length}
/>
{filteredNotes.map(({ id, date, text }) => (
<NoteButton key={id} id={id} date={date} text={text} />
))}
</>
);
}
The solution for React 17 and below is to debounce for 1000ms.
+ const debounce = (fn, delay) => {
+ let timeoutId;
+ return (...args) => {
+ clearTimeout(timeoutId);
+ timeoutId = setTimeout(() => fn(...args), delay);
+ };
+ };
// Within the onChange handler
onChange={() => {
+ setFilterInput(value);
+ debounce(() => {
- setFilter(filterInput);
+ setFilterValue(value);
+ }, 1000);
}}
- Tags: context, requires_import, requires_package
If a context is used in multiple places, and one of the values changes, all components that use the context will re-render.
We can break up the context into multiple contexts for each independent "responsibility".
+ import { createContext, useContext } from 'react';
+ import { useContextSelector } from 'use-context-selector';
+ const DarkModeContext = createContext();
+ const UserContext = createContext();
- const AppContext = createContext();
function App() {
const [mode, setMode] = useState('light');
const [user, setUser] = useState({ name: 'John' });
return (
- <AppContext.Provider value={{ mode, setMode, user, setUser }}>
+ <DarkModeContext.Provider value={{ mode, setMode }}>
+ <UserContext.Provider value={{ user, setUser }}>
<Main />
+ </UserContext.Provider>
+ </DarkModeContext.Provider>
);
}
function Main() {
- const { mode, setMode, user, setUser } = useContext(AppContext);
+ const mode = useContextSelector(DarkModeContext, (context) => context.mode);
+ const setMode = useContextSelector(
+ DarkModeContext,
+ (context) => context.setMode
+ );
+ const user = useContextSelector(UserContext, (context) => context.user);
+ const setUser = useContextSelector(
+ UserContext,
+ (context) => context.setUser
+ );
return (
<>
<Header />
<Content />
</>
);
}
- Tags: requires_import, requires_package
If you are not on React 18 or above, then multiple hooks will cause multiple renders.
If you are on React 17, you can use unstable_batchedUpdates to batch multiple hooks.
const [one, setOne] = useState(1);
const [two, setTwo] = useState(2);
const increment = () => {
+ unstable_batchedUpdates(() => {
setOne(3);
setTwo(4);
+ });
}
If you are on React 16 or below, you can use reducer and call dispatch instead of multiple hooks.
- const [one, setOne] = useState(1);
- const [two, setTwo] = useState(2);
+ const [state, dispatch] = useReducer((state) => {
+ return { one: state.one + 1, two: state.two + 1 };
+ }, { one: 1, two: 2 });
const increment = () => {
- setOne(3);
- setTwo(4);
+ dispatch();
}
- Tags: expensive_operation
useCallback and useMemo are used to memoize values. If the value is not expensive to compute, then it's not necessary to use them. Performance optimizations are not free. They ALWAYS come with a cost but do NOT always come with a benefit to offset that cost. Therefore, optimize responsibly.
There are specific reasons both of these hooks are built-into React:
- Referential equality
- Computationally expensive calculations
If onClick is not passed as a prop to a child component, We can remove useCallback.
- const onClick = useCallback(() => {
+ const onClick = () => {
setCandies(allCandies => allCandies.filter(c => c !== candy))
- }, []);
+ };
When passing a callback as a prop to a child component, we should wrap it in useCallback to maintain referential equality and prevent unnecessary rerenders.
+ import { useCallback } from 'react';
function Parent({ id }) {
+ const onClick = useCallback(() => someFunction(id), [someFunction, id]);
return <Child onClick={onClick} />;
}
function Child({ onClick }) {
return <button onClick={onClick}></button>;
}
Notice that candy is not expensive to compute. We can remove useMemo.
- const initialCandies = ['snickers', 'skittles', 'twix', 'milky way']
+ const initialCandies = React.useMemo(
+ () => ['snickers', 'skittles', 'twix', 'milky way'],
+ [],
+ )
In this particular scenario, what would be even better is to make this change:
+ const initialCandies = ['snickers', 'skittles', 'twix', 'milky way']
function CandyDispenser() {
- const initialCandies = ['snickers', 'skittles', 'twix', 'milky way']
const [candies, setCandies] = React.useState(initialCandies)
Notice that candy is expensive to compute. We can add useMemo.
const getCandy = () => {
// expensive computation
}
- const candy = () => getCandy();
+ const candy = useMemo(() => getCandy(), []);
- Tags: expensive_operation, requires_import, requires_package
If you have a long list of items, rendering all of them will slow down the app. Virtualization is a technique to only render the items that are visible to the user.
You can use any of the following libraries to virtualize the list: @tanstack/react-virtual or react-window. Let's use react-window to virtualize the list. This is a very contrived example, make sure you read the docs to understand how to use it.
+ import { FixedSizeList as List } from 'react-window';
function Item({ index, style }) {
return <div style={style}>{item.name}</div>;
}
function List({ items }) {
return (
+ <List
+ height={150}
+ itemCount={items.length}
+ itemSize={30}
+ width={300}
+ >
- <div>
- {items.map((item, index) => (
- <Item key={item.id} index={index} style={{ height: '30px' }} />
- ))}
- </div>
+ {Item}
+ </List>
);
}
- Tags: expensive_operation, requires_import, requires_package
If you have a long list of items that are rendered on the server, the hydration cost will slow down the intial load. Lazy hydration is a technique to defer hydration until the user scrolls to the item.
Use react-lazy-hydration to defer hydration until the user scrolls to the item. In this example, MarkdownBlock is expensive to hydrate, so we wrap it in LazyHydrate.
+ import { LazyHydrate } from 'react-lazy-hydration';
function List({ items }) {
return (
<div>
{items.map((item, index) => (
+ <LazyHydrate whenVisible key={index}>
<MarkdownBlock>{item}</MarkdownBlock>
+ </LazyHydrate>
))}
</div>
);
}
- Tags: expensive_operation, requires_import
If a specific child component is expensive to render, we can wrap it in useMemo to memoize it.
+ import { useMemo } from 'react';
function Parent({ id }) {
return (
- <ReactMarkdown content={content} />
+ {useMemo(() => <ReactMarkdown content={content} />, [content])}
);
}
- Tags: unstable_reference
If you see this pattern: prop={object}
or prop={array}
, then the child component will re-render if the value is undefined.
Make sure to add a default value to the prop to prevent unnecessary rerenders.
const PostList = memo(({ posts }) => {
console.log('PostList rendered');
return (
<div>
{posts.map((post, index) => (
<div key={index}>{post}</div>
))}
</div>
);
});
function App({ posts }) {
- return <PostList posts={props.posts} />
+ return <PostList posts={props.posts || []} />
}
- Tags: unstable_reference, memo, requires_import
useContext will cause the component to re-render if the value changes. If the value is an object or array, then it will re-render if the reference changes.
By isolating the useContext to it's relevant child component, we can prevent unnecessary rerenders.
function App({ list }) {
- const user = useContext(UserContext);
return (
<div>
<div>
{list.map((item) => (
<ReactMarkdown content={item}>
))}
</div>
- <div>{user.name}</div>
+ <User />
</div>
);
+ function User() {
+ const user = useContext(UserContext);
+ return <div>{user.name}</div>;
+ }
Make sure to memo the default value to the context to prevent unnecessary rerenders.
import { createContext, useContext } from 'react';
import { useMemo } from 'react';
const UserContext = createContext();
function App() {
const [user, setUser] = useState({ name: 'John' });
return (
- <UserContext.Provider value={{ user, setUser }}>
+ <UserContext.Provider value={useMemo(() => ({ user, setUser }), [user])}>
<Main />
+ </UserContext.Provider>
);
}
- Tags: unstable_reference, memo, requires_import
TODO: add example:
- Tags: expensive_operation, memo, requires_import, bad_selector
useSelector is called on every render. If the selector is expensive, it will slow down the app.
The solution is to use useMemo to memoize the selector.
+ import { useMemo } from 'react';
- const activeUsers = useSelector((state) =>
- state.users.filter((i) => new Date(i.loginDate).getFullYear() === 2023)
- );
+ const users = useSelector((state) => state.users);
+ const activeUsers = useMemo(
+ () =>
+ activeThisMonth.filter((i) => new Date(i.loginDate).getFullYear() === 2023),
+ [users]
+ );
- Tags: expensive_operation, requires_package, requires_import
Spread is slow, especially for large objects, as it needs to make a copy of the object.
Immer's produce
is faster than spread, and it's also immutable.
+ import { produce } from 'immer';
const userReducer = (userData = [], action) => {
if (action.type === 'UPDATE_USER') {
- const [currentUser, ...otherUsers] = userData;
- return [
- {
- ...currentUser,
- loginDate: action.payload.dateString,
- },
- ...otherUsers,
- ];
+ return produce(userData, (userData) => {
+ userData[0].loginDate = action.payload.dateString;
+ });
}
return userData;
};
You can also use @reduxjs/toolkit, which uses immer internally.
+ import { createSlice } from '@reduxjs/toolkit';
const userSlice = createSlice({
name: 'user',
initialState: [],
reducers: {
updateUser: (state, action) => {
- const [currentUser, ...otherUsers] = state;
- return [
- {
- ...currentUser,
- loginDate: action.payload.dateString,
- },
- ...otherUsers,
- ];
+ state[0].loginDate = action.payload.dateString;
},
},
});
=== in JavaScript: {} !== {}, [] !== [], () => {} !== () => {} === in JavaScript: 1 === 1, "str" === "str", null === null