Most React performance problems are not architectural. They are not about picking the wrong state manager or choosing the wrong rendering strategy. They are small habit things that look perfectly fine in isolation but compound quietly across a codebase until your app feels sluggish and you are not sure why.
This article covers five of the most common ones, with code examples so you can see exactly what to change.
1. Inline objects and functions in JSX
This one is everywhere. You write a style or a handler directly inside JSX and it looks clean. The problem is that JavaScript creates a new object or function reference on every render. React does a shallow comparison when deciding whether to re-render a child, and since the reference is always new, the child always re-renders, even when nothing meaningful has changed.
The bad pattern:
<Card style={{ padding: 16, borderRadius: 8 }} onClick={() => handleClick(id)} />
The fix:
const cardStyle = { padding: 16, borderRadius: 8 }
function Parent() {
const handleClick = useCallback((id) => {
// handle it
}, [])
return <Card style={cardStyle} onClick={handleClick} />
}
Move static objects outside the component entirely. For functions that depend on state or props, useCallback is appropriate, but only when the child that receives it is memoized. Otherwise you are optimizing nothing.
2. Overusing useMemo and useCallback
This one tends to get worse as developers get more experienced, because it feels like you are being careful. The reality is that memoization has its own cost. React has to store the previous value, compare dependencies, and decide whether to recompute. For cheap computations, that overhead often costs more than just running the calculation again.
Unnecessary memoization:
const fullName = useMemo(() => {
return `${firstName} ${lastName}`
}, [firstName, lastName])
That is a string concatenation. It does not need useMemo.
When it actually makes sense:
const sortedList = useMemo(() => {
return items.sort((a, b) => b.score - a.score)
}, [items])
Sorting a large array on every render is the kind of work worth memoizing. The rule is simple: profile first, then memoize only what measurement shows is actually slow.
3. One giant component doing everything
You know this component. It fetches data, manages three different pieces of state, handles user interactions, and renders a full page of UI all in one place. Every state update, no matter how small, triggers a re-render of the entire thing.
The fix is not complicated. Split your components into two types:
Container components handle the logic. They manage state, make API calls, and pass data down as props. They do not render complex UI.
Presentational components are pure display. They receive props and render. Because they have no local state, they only re-render when their props actually change, and wrapping them in React.memo becomes straightforward and effective.
// Container
function UserProfileContainer() {
const { data, isLoading } = useUserProfile()
if (isLoading) return <Spinner />
return <UserProfileCard user={data} />
}
// Presentational
const UserProfileCard = React.memo(function UserProfileCard({ user }) {
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
)
})
This pattern also makes testing much easier as a side benefit.
4. Storing derived state in useState
If a value can be computed from props or from state you already have, it does not need its own useState. Storing it separately means you have two sources of truth for the same piece of data, and keeping them in sync requires extra code that is easy to get wrong.
The bad pattern:
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const [fullName, setFullName] = useState('')
useEffect(() => {
setFullName(`${firstName} ${lastName}`)
}, [firstName, lastName])
This causes a double render every time firstName or lastName changes, because the state update inside useEffect triggers another render cycle.
The fix:
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const fullName = `${firstName} ${lastName}`
Compute it during render. It is simpler, it is always in sync, and it renders once instead of twice.
5. Using array index as a list key
React uses keys to track which items in a list have changed between renders. When you use the array index as the key, React cannot tell the difference between an item that moved and an item that changed. This leads to wrong state being mapped to the wrong component, animation bugs, and slower reconciliation.
The bad pattern:
items.map((item, index) => (
<TodoItem key={index} item={item} />
))
Reorder the list, remove an item from the middle, or insert at the top, and React will get confused about which component is which.
The fix:
items.map((item) => (
<TodoItem key={item.id} item={item} />
))
Always use a stable, unique identifier. If your data does not have IDs, generate them when the data is created, not at render time.
Wrapping up
None of these are obscure edge cases. They show up in production codebases every day, including ones written by experienced engineers. The reason they persist is that they rarely cause obvious crashes, they just quietly make things slower.
The best way to catch them is a combination of code review habits and the React DevTools Profiler. Record a session, look at what is re-rendering more than it should, and work backwards from there.
If this was useful, I write about React, Flutter, and frontend engineering regularly. Follow along for more.
Top comments (0)