Usually when a new software development team forms there is some negotiating how exactly the team wants to write their code. When the maintainers of a programming language or major framework are opinionated on these things, this can help provide a default starting point for these discussions.
React is known for not being very opinionated in some ways, but the latest React docs actually make quite a few recommendations about how you write your React code! Since these docs were first released in beta form, other agents and I have been referring to them more and more as a basis for conversations about code style we have on our teams.
In this post I'll highlight the recommendations from the React docs that come up most frequently in my team discussions about React code style.
For each I'll share a brief summary and usually a code snippet. Much more information and rationale can be found by following the included link to the relevant portion of the React docs. When there is an ESLint rule that can enforce the recommendation, I've linked to that rule as well—thank you to Lorenzo Sciandra for the idea!
Some of these recommendations might feel like code style opinions with no real consequences. However, as the React docs explain, in each case straying from the recommendation comes with costs or risks. You're free to make your own decision, but keep in mind that the React core team has thought through these things a lot more than you or I have. This doesn’t mean they’re always right, but it's a good idea to at least hear them out.
- When choosing a key for an element in a loop, use an identifier that will always be the same for the same entry—not an array index
- When defining a component, define it at the top level of the file/module, not nested inside another component or function
- When deciding what to store in state, store the minimal representation that can be used to compute what you need
- When considering whether to cache with useMemo, useCallback, or React.memo, defer caching until there is an observed performance problem
- When extracting shared code into a function, only name it as a hook if it calls other hooks
- When you need to adjust state in response to a prop change, set the state directly in the component function (during rendering), not in an effect
- When you need to fetch data, prefer using a library over useEffect
- When you need to take an action in response to an event occurring, write the code in an event handler, not in a useEffect
- When a useEffect dependency is causing rerenders you don't want (including infinite loops), don't just remove the dependency from the array: remove the dependency from the effect function too
1. When choosing a key for an element in a loop, use an identifier that will always be the same for the same entry—not an array index
React uses the key to keep track of list elements across renders. If an element is added, deleted, or reordered then index keys will mislead React, which can lead to bugs.
// 🛑 WRONG
return (
<ul>
{items.map((item, index) => (
<li key={index}>…</li>
))}
</ul>
);
// 🟢 RIGHT, assuming item.id is a stable unique identifier
return (
<ul>
{items.map((item, index) => (
<li key={item.id}>…</li>
))}
</ul>
);
2. When defining a component, define it at the top level of the file/module, not nested inside another component or function
Sometimes it can seem convenient to define a component inside another component. But this will result in the component being treated as different on every render, leading to slow performance.
// 🛑 WRONG
function ParentComponent() {
// ...
function ChildComponent() {…}
return <div><ChildComponent /></div>;
}
// 🟢 RIGHT
function ChildComponent() {…}
function ParentComponent() {
return <div><ChildComponent /></div>;
}
3. When deciding what to store in state, store the minimal representation that can be used to compute what you need
This makes state easy to update without introducing bugs, because it precludes different state items falling out of date with one another or otherwise becoming inconsistent.
React docs on structuring state
// 🛑 WRONG
const [allItems, setAllItems] = useState([]);
const [urgentItems, setUrgentItems] = useState([]);
function handleSomeEvent(newItems) {
setAllItems(newItems);
setUrgentItems(newItems.filter(item => item.priority === 'urgent'));
}
// 🟢 RIGHT
const [allItems, setAllItems] = useState([]);
const urgentItems = allItems.filter(item => item.priority === 'urgent');
function handleSomeEvent(newItems) {
setAllItems(newItems);
}
4. When considering whether to cache with useMemo, useCallback, or React.memo, defer caching until there is an observed performance problem.
Although there is not a major downside to always memoizing, the minor downside is that it makes the code less readable.
Read more in the React docs on useMemo, React docs on useCallback, or React docs on React.memo
// 🛑 WRONG
const [allItems, setAllItems] = useState([]);
const urgentItems = useMemo(() => (
allItems.filter(item => item.status === 'urgent'
), [allItems]);
// 🟢 RIGHT (until an observed performance problem)
const [allItems, setAllItems] = useState([]);
const urgentItems = allItems.filter(item => item.priority === 'urgent');
5. When extracting shared code into a function, only name it as a hook if it calls other hooks
If your function calls other hooks, it needs to be a hook itself, so that React can enforce the restrictions on hooks that allow them to work. If your function does not call other hooks, then there’s no reason to opt in to those restrictions. Your function will be more versatile as a non-hook because it can be called from anywhere, including within conditionals.
React docs on the "use" prefix
// 🛑 WRONG
function useDateColumnConfig() { // will be subject to hooks restrictions
return {
dataType: 'date',
formatter: prettyFormatDate,
editorComponent: DateEditor,
};
}
// 🟢 RIGHT
function getDateColumnConfig() { // can be called anywhere
return {
dataType: 'date',
formatter: prettyFormatDate,
editorComponent: DateEditor,
};
}
function useNameColumnConfig() { // has to be a hook since it calls a hook: useTranslation
const { t } = useTranslation();
return {
dataType: 'string',
title: t('columns.name'),
};
}
6. When you need to adjust state in response to a prop change, set the state directly in the component function (during rendering), not in an effect
If you're planning to adjust state in response to a prop change, it's a good idea to first confirm you really need to. It's preferred if you can instead derive the data during rendering (see recommendation 3, above) or use a key to reset all of the state.
If you do need to adjust part of the state, it's helpful to consider a key point the React docs make about effects: they “are an escape hatch from the React paradigm. They let you ‘step outside’ of React and synchronize your components with some external system…” That complexity is not needed when you just need to do a quick state update in response to a prop change.
React docs on state changes from props
Thanks to the React docs for the example code this (slightly simplified) snippet is based on!
// 🛑 WRONG
function List({ items }) {
const [selection, setSelection] = useState(null);
useEffect(() => {
setSelection(null);
}, [items]);
//...
}
// 🟢 RIGHT
function List({ items }) {
const [prevItems, setPrevItems] = useState(items);
const [selection, setSelection] = useState(null);
if (items !== prevItems) {
setPrevItems(items);
setSelection(null);
}
//...
}
7. When you need to fetch data, prefer using a library over useEffect
Subtle bugs can occur with useEffect data fetching unless you write a lot of boilerplate to handle them. The React docs provide a number of suggestions for good data fetching libraries.
// 🛑 WRONG
const [items, setItems] = useState();
useEffect(() => {
api.loadItems().then(newItems => setItems(newItems));
}, []);
// 🟢 RIGHT (one library option)
import {useQuery} from '@tanstack/react-query';
const { data: items } = useQuery(['items'], () => api.loadItems());
8. When you need to take an action in response to an event occurring, write the code in an event handler, not in a useEffect
This ensures the code will run only once per event occurrence.
Read the React docs on effects vs. events, or watch a conference talk on effects vs. events.
const [savedData, setSavedData] = useState(null);
const [validationErrors, setValidationErrors] = useState(null);
// 🛑 WRONG
useEffect(() => {
if (savedData) {
setValidationErrors(null);
}
}, [savedData]);
function saveData() {
const response = await api.save(data);
setSavedData(response.data);
}
// 🟢 RIGHT
async function saveData() {
const response = await api.save(data);
setSavedData(response.data);
setValidationErrors(null);
}
9. When a useEffect dependency is causing rerenders you don't want (including infinite loops), don't just remove the dependency from the array: remove the dependency from the effect function too
It can be hard to understand why making this change is worth the effort; to do so, I recommend reading the pages that the React docs devote to useEffect. In brief, using dependencies that you don't list in the dependency array probably means that the effect is being used for something other than what effects are intended for: synchronizing. This is likely to lead to difficult-to-diagnose bugs sooner or later.
Putting it into practice
I hope these points from the React docs help you learn a new technique, understand a technique more deeply, or explain a technique to others.
Do you follow these recommendations on your React projects? Are there other recommendations from the React docs that come up frequently for you?