How to write performant React code: rules, patterns, do's and don'ts
Performance and React! Such a fun topic with so many controversial opinions and so many best practices flipping to be the opposite in just 6 months. Is it even possible to say anything definitive here or to make any generalized recommendations?
Usually, performance experts are the proponents of “premature optimisation is the root of all evil” and “measure first” rules. Which loosely translates into “don’t fix that is not broken” and is quite hard to argue with. But I’m going to anyway 😉
What I like about React, is that it makes implementing complicated UI interactions incredibly easy. What I don’t like about React, is that it also makes it incredibly easy to make mistakes with huge consequences that are not visible right away. The good news is, it’s also incredibly easy to prevent those mistakes and write code that is performant most of the time right away, thus significantly reducing the time and effort it takes to investigate performance problems since there will be much fewer of those. Basically, “premature optimisation”, when it comes to React and performance, can actually be a good thing and something that everyone should do 😉. You just need to know a few patterns to watch out for in order to do that meaningfully.
So this is exactly what I want to prove in this article 😊. I’m going to do that by implementing a “real-life” app step by step, first in a “normal” way, using the patterns that you’ll see practically everywhere and surely used multiple times by yourself. And then refactor each step with performance in mind, and extract a generalized rule from every step that can be applied to most apps most of the time. And then compare the result in the end.
Let’s begin!
We are going to write one of the “settings” page for an online shop (that we introduced into the previous “Advanced typescript for React developers” articles). On this page, users will be able to select a country from the list, see all the information available for this country (like currency, delivery methods, etc), and then save this country as their country of choice. The page would look something like this:

On the left we’ll have a list of countries, with “saved” and “selected” states, when an item in the list is clicked, in the column on the right the detailed information is shown. When the “save” button is pressed, the “selected” country becomes “saved”, with the different item colour.
Oh, and we’d want the dark mode there of course, it’s 2022 after all!
Also, considering that in 90% of the cases performance problems in React can be summarised as “too many re-renders”, we are going to focus mostly on reducing those in the article. (Another 10% are: “renders are too heavy” and “really weird stuff that need further investigation”.)
Let's structure our app first
First of all, let's take a look at the design, draw imaginary boundaries, and draft the structure of our future app and which components we’d need to implement there:
- a root “Page” component, where we’d handle the “submit” logic and country selection logic
- a “List of countries” component, that would render all the countries in a list, and in the future handle things like filtering and sorting
- “Item” component, that renders the country in the “List of countries”
- a “Selected country” component, that renders detailed information about the selected country and has the “Save” button

This is, of course, not the only possible way to implement this page, that’s the beauty and the curse of React: everything can be implemented in a million ways and there is no right or wrong answer for anything. But there are some patterns that in the long run in fast-growing or large already apps can definitely be called “never do this” or “this is a must-have”.
Let’s see whether we can figure them out together 🙂
Implementing Page component
Now, finally, the time to get our hands dirty and do some coding. Let’s start from the “root” and implement the Page component.
First: we need a wrapper with some styles that renders page title, “List of countries” and “Selected country” components.
Second: out page should receive the list of countries from somewhere, and then pass it to the CountriesList
component so that it could render those.
Third: our page should have an idea of a “selected” country, that will be received from the CountriesList
component and passed to the SelectedCountry
component.
And finally: our page should have an idea of a “saved” country, that will be received from the SelectedCountry
component and passed to the CountriesList
component (and be sent to the backend in the future).
1export const Page = ({ countries }: { countries: Country[] }) => {2 const [selectedCountry, setSelectedCountry] = useState<Country>(countries[0]);3 const [savedCountry, setSavedCountry] = useState<Country>(countries[0]);45 return (6 <>7 <h1>Country settings</h1>8 <div css={contentCss}>9 <CountriesList10 countries={countries}11 onCountryChanged={(c) => setSelectedCountry(c)}12 savedCountry={savedCountry}13 />14 <SelectedCountry15 country={selectedCountry}16 onCountrySaved={() => setSavedCountry(selectedCountry)}17 />18 </div>19 </>20 );21};
That is the entire implementation of the “Page” component, it’s the most basic React that you can see everywhere, and there is absolutely nothing criminal in this implementation. Except for one thing. Curious, can you see it?
Refactoring Page component - with performance in mind
I think it is common knowledge by now, that react re-renders components when there is a state or props change. In our Page component when setSelectedCountry
or setSavedCountry
is called, it will re-render. If the countries array (props) in our Page component changes, it will re-render. And the same goes for CountriesList
and SelectedCountry
components - when any of their props change, they will re-render.
Also, anyone, who spent some time with React, knows about javascript equality comparison, the fact that React does strict equality comparison for props, and the fact that inline functions create new value every time. This leads to the very common (and absolutely wrong btw) belief, that in order to reduce re-renders of CountriesList
and SelectedCountry
components we need to get rid of re-creating inline functions on every render by wrapping inline functions in useCallback
. Even React docs mention useCallback
in the same sentence with “prevent unnecessary renders”! See whether this pattern looks familiar:
1export const Page = ({ countries }: { countries: Country[] }) => {2 // ... same as before34 const onCountryChanged = useCallback((c) => setSelectedCountry(c), []);5 const onCountrySaved = useCallback(() => setSavedCountry(selectedCountry), []);67 return (8 <>9 ...10 <CountriesList11 onCountryChanged={onCountryChange}12 />13 <SelectedCountry14 onCountrySaved={onCountrySaved}15 />16 ...17 </>18 );19};
Do you know the funniest part about it? It actually doesn’t work. Because it doesn’t take into account the third reason why React components are re-rendered: when the parent component is re-rendered. Regardless of the props, CountriesList
will always re-render if Page is re-rendered, even if it doesn’t have any props at all.
We can simplify the Page example into just this:
1const CountriesList = () => {2 console.log("Re-render!!!!!");3 return <div>countries list, always re-renders</div>;4};56export const Page = ({ countries }: { countries: Country[] }) => {7 const [counter, setCounter] = useState<number>(1);89 return (10 <>11 <h1>Country settings</h1>12 <button onClick={() => setCounter(counter + 1)}>13 Click here to re-render Countries list (open the console) {counter}14 </button>15 <CountriesList />16 </>17 );18};
And every time we click the button, we’ll see that CountriesList
is re-rendered, even if it doesn’t have any props at all. Codesandbox code is here.
And this, finally, allows us to solidify the very first rule of this article:
Now, there are a few ways to deal with situations like the above, I am going to use the simplest one for this particular occasion: useMemo hook. What it does is it’s essentially “caches” the results of whatever function you pass into it, and only refreshes them when a dependency of useMemo
is changed. If I just extract the rendered CountriesList
into a variable const list = <ComponentList />;
and then apply useMemo
on it, the ComponentList
component now will be re-rendered only when useMemo dependencies will change.
1export const Page = ({ countries }: { countries: Country[] }) => {2 const [counter, setCounter] = useState<number>(1);34 const list = useMemo(() => {5 return <CountriesList />;6 }, []);78 return (9 <>10 <h1>Country settings</h1>11 <button onClick={() => setCounter(counter + 1)}>12 Click here to re-render Countries list (open the console) {counter}13 </button>14 {list}15 </>16 );17};
Which in this case is never, since it doesn’t have any dependencies. This pattern basically allows me to break out of this “parent re-renders - re-render all the children regardless” loop and take control over it. Check out the full example in codesandbox.
The most important thing there to be mindful of is the list of dependencies of useMemo
. If it depends on exactly the same thing that causes the parent component to re-render, then it’s going to refresh its cache with every re-render, and essentially becomes useless. For example, if in this simplified example I pass the counter
value as a dependency to the list
variable (notice: not even a prop to the memoised component!), that will cause useMemo
to refresh itself with every state change and will make CountriesList
re-render again.
See the codesandbox example.1const list = useMemo(() => {2 return (3 <>4 {counter}5 <CountriesList />6 </>7 );8}, [counter]);
Okay, so all of this is great, but how exactly it can be applied to our non-simplified Page component? Well, if we look closely to its implementation again
1export const Page = ({ countries }: { countries: Country[] }) => {2 const [selectedCountry, setSelectedCountry] = useState<Country>(countries[0]);3 const [savedCountry, setSavedCountry] = useState<Country>(countries[0]);45 return (6 <>7 <h1>Country settings</h1>8 <div css={contentCss}>9 <CountriesList10 countries={countries}11 onCountryChanged={(c) => setSelectedCountry(c)}12 savedCountry={savedCountry}13 />14 <SelectedCountry15 country={selectedCountry}16 onCountrySaved={() => setSavedCountry(selectedCountry)}17 />18 </div>19 </>20 );21};
we’ll see that:
selectedCountry
state is never used inCountriesList
componentsavedCountry
state is never used inSelectedCountry
component

Which means that when selectedCountry
state changes, CountriesList
component doesn’t need to re-render at all! And the same story with savedCountry
state and SelectedCountry
component. And I can just extract both of them to variables and memoise them to prevent their unnecessary re-renders:
1export const Page = ({ countries }: { countries: Country[] }) => {2 const [selectedCountry, setSelectedCountry] = useState<Country>(countries[0]);3 const [savedCountry, setSavedCountry] = useState<Country>(countries[0]);45 const list = useMemo(() => {6 return (7 <CountriesList8 countries={countries}9 onCountryChanged={(c) => setSelectedCountry(c)}10 savedCountry={savedCountry}11 />12 );13 }, [savedCountry, countries]);1415 const selected = useMemo(() => {16 return (17 <SelectedCountry18 country={selectedCountry}19 onCountrySaved={() => setSavedCountry(selectedCountry)}20 />21 );22 }, [selectedCountry]);2324 return (25 <>26 <h1>Country settings</h1>27 <div css={contentCss}>28 {list}29 {selected}30 </div>31 </>32 );33};
And this, finally, lets us formalize the second rule of this article:
Implementing the list of countries
Now, that our Page component is ready and perfect, time to flesh out its children. First, let’s implement the complicated component: CountriesList
. We already know, that this component should accept the list of countries, should trigger onCountryChanged
callback when a country is selected in the list, and should highlight the savedCountry
into a different color, according to design. So let’s start with the simplest approach:
1type CountriesListProps = {2 countries: Country[];3 onCountryChanged: (country: Country) => void;4 savedCountry: Country;5};67export const CountriesList = ({8 countries,9 onCountryChanged,10 savedCountry11}: CountriesListProps) => {12 const Item = ({ country }: { country: Country }) => {13 // different className based on whether this item is "saved" or not14 const className = savedCountry.id === country.id ? "country-item saved" : "country-item";1516 // when the item is clicked - trigger the callback from props with the correct country in the arguments17 const onItemClick = () => onCountryChanged(country);18 return (19 <button className={className} onClick={onItemClick}>20 <img src={country.flagUrl} />21 <span>{country.name}</span>22 </button>23 );24 };2526 return (27 <div>28 {countries.map((country) => (29 <Item country={country} key={country.id} />30 ))}31 </div>32 );33};
Again, the simplest component ever, only 2 things are happening there, really:
- we generate the
Item
based on the props we receive (it depends on bothonCountryChanged
andsavedCountry
) - we render that
Item
for all countries in a loop
And again, there is nothing criminal about any of this per se, I have seen this pattern used pretty much everywhere.
Refactoring List of countries component - with performance in mind
Time again to refresh a bit our knowledge of how React renders things, this time - what will happen if a component, like Item
component from above, is created during another component render? Short answer - nothing good, really. From React’s perspective, this Item
is just a function that is new on every render, and that returns a new result on every render. So what it will do, is on every render it will re-create results of this function from scratch, i.e. it will just compare the previous component state with the current one, like it happens during normal re-render. It will drop the previously generated component, including its DOM tree, remove it from the page, and will generate and mount a completely new component, with a completely new DOM tree every single time the parent component is re-rendered.
If we simplify the countries example to demonstrate this effect, it will be something like this:
1const CountriesList = ({ countries }: { countries: Country[] }) => {2 const Item = ({ country }: { country: Country }) => {3 useEffect(() => {4 console.log("Mounted!");5 }, []);6 console.log("Render");7 return <div>{country.name}</div>;8 };910 return (11 <>12 {countries.map((country) => (13 <Item country={country} />14 ))}15 </>16 );17};
This is the heaviest operation from them all in React. 10 “normal” re-renders is nothing compared to the full re-mounting of a freshly created component from a performance perspective. In normal circumstances, useEffect
with an empty dependencies array would be triggered only once - after the component finished its mounting and very first rendering. After that the light-weight re-rendering process in React kicks in, and component is not created from scratch, but only updated when needed (that’s what makes React so fast btw). Not in this scenario though - take a look at this codesandbox, click on the “re-render” button with open console, and enjoy 250 renders AND mountings happening on every click.
The fix for this is obvious and easy: we just need to move the Item
component outside of the render function.
1const Item = ({ country }: { country: Country }) => {2 useEffect(() => {3 console.log("Mounted!");4 }, []);5 console.log("Render");6 return <div>{country.name}</div>;7};89const CountriesList = ({ countries }: { countries: Country[] }) => {10 return (11 <>12 {countries.map((country) => (13 <Item country={country} />14 ))}15 </>16 );17};
Now in our simplified codesandbox mounting doesn’t happen on every re-render of the parent component.
As a bonus, refactoring like this helps maintain healthy boundaries between different components and keep the code cleaner and more concise. This is going to be especially visible when we apply this improvement to our “real” app. Before:
1export const CountriesList = ({2 countries,3 onCountryChanged,4 savedCountry5}: CountriesListProps) => {67 // only "country" in props8 const Item = ({ country }: { country: Country }) => {9 // ... same code10 };1112 return (13 <div>14 {countries.map((country) => (15 <Item country={country} key={country.id} />16 ))}17 </div>18 );19};
After:
1type ItemProps = {2 country: Country;3 savedCountry: Country;4 onItemClick: () => void;5};67// turned out savedCountry and onItemClick were also used8// but it was not obvious at all in the previous implementation9const Item = ({ country, savedCountry, onItemClick }: ItemProps) => {10 // ... same code11};1213export const CountriesList = ({14 countries,15 onCountryChanged,16 savedCountry17}: CountriesListProps) => {18 return (19 <div>20 {countries.map((country) => (21 <Item22 country={country}23 key={country.id}24 savedCountry={savedCountry}25 onItemClick={() => onCountryChanged(country)}26 />27 ))}28 </div>29 );30};
Now, that we got rid of the re-mounting of Item
component every time the parent component is re-rendered, we can extract the third rule of the article:
Implementing selected country
Next step: the “selected country” component, which is going to be the shortest and the most boring part of the article, since there is nothing to show there really: it’s just a component that accepts a property and a callback, and renders a few strings:
1const SelectedCountry = ({ country, onSaveCountry }: { country: Country; onSaveCountry: () => void }) => {2 return (3 <>4 <ul>5 <li>Country: {country.name}</li>6 ... // whatever country's information we're going to render7 </ul>8 <button onClick={onSaveCountry} type="button">Save</button>9 </>10 );11};
🤷🏽♀️ That’s it! It’s only here just to make the demo codesandbox more interesting 🙂
Final polish: theming
And now the final step: dark mode! Who doesn’t love those? Considering that the current theme should be available in most components, passing it through props everywhere would be a nightmare, so React Context is the natural solution here.
Creating theme context first:
1type Mode = 'light' | 'dark';2type Theme = { mode: Mode };3const ThemeContext = React.createContext<Theme>({ mode: 'light' });45const useTheme = () => {6 return useContext(ThemeContext);7};
Adding context provider and the button to switch it to the Page component:
1export const Page = ({ countries }: { countries: Country[] }) => {2 // same as before3 const [mode, setMode] = useState<Mode>("light");45 return (6 <ThemeContext.Provider value={{ mode }}>7 <button onClick={() => setMode(mode === 'light' ? 'dark' : 'light')}>Toggle theme</button>8 // the rest is the same as before9 </ThemeContext.Provider>10 )11}
And then using the context hook to color our buttons in the appropriate theme:
1const Item = ({ country }: { country: Country }) => {2 const { mode } = useTheme();3 const className = `country-item ${mode === "dark" ? "dark" : ""}`;4 // the rest is the same5}
Again, nothing criminal in this implementation, a very common pattern, especially for theming.
Refactoring theming - with performance in mind.
Before we’ll be able to catch what’s wrong with the implementation above, time to look into a fourth reason why a React component can be re-rendered, that often is forgotten: if a component uses context consumer, it will be re-rendered every time the context provider’s value is changed.
Remember our simplified example, where we memoised the render results to avoid their re-renders?
1const Item = ({ country }: { country: Country }) => {2 console.log("render");3 return <div>{country.name}</div>;4};56const CountriesList = ({ countries }: { countries: Country[] }) => {7 return (8 <>9 {countries.map((country) => (10 <Item country={country} />11 ))}12 </>13 );14};1516export const Page = ({ countries }: { countries: Country[] }) => {17 const [counter, setCounter] = useState<number>(1);1819 const list = useMemo(() => <CountriesList countries={countries} />, [20 countries21 ]);2223 return (24 <>25 <h1>Country settings</h1>26 <button onClick={() => setCounter(counter + 1)}>27 Click here to re-render Countries list (open the console) {counter}28 </button>29 {list}30 </>31 );32};
Page
component will re-render every time we click the button since it updates the state on every click. But CountriesList
is memoised and is independent of that state, so it won’t re-render, and as a result Item
component won’t re-render as well. See the codesandbox here.
Now, what will happen if I add the Theme context here? Provider in the Page
component:
1export const Page = ({ countries }: { countries: Country[] }) => {2 // everything else stays the same34 // memoised list is still memoised5 const list = useMemo(() => <CountriesList countries={countries} />, [6 countries7 ]);89 return (10 <ThemeContext.Provider value={{ mode }}>11 // same12 </ThemeContext.Provider>13 );14};
And context in the Item component:
1const Item = ({ country }: { country: Country }) => {2 const theme = useTheme();3 console.log("render");4 return <div>{country.name}</div>;5};
If they were just normal components and hooks, nothing would’ve happened - Item
is not a child of Page
component, CountriesList
won’t re-render because of memoisation, so Item
wouldn’t either. Except, in this case, it’s a Provider-consumer combination, so every time the value on the provider changes, all of the consumers will re-render. And since we’re passing new object to the value all the time, Items
will unnecessary re-render on every counter. Context basically bypasses the memorisation we did and makes it pretty much useless. See the codesandbox.
The fix to it, as you might have already guessed, is just to make sure that the value
in the provider doesn’t change more than it needs to. In our case, we just need to memoise it as well:
1export const Page = ({ countries }: { countries: Country[] }) => {2 // everything else stays the same34 // memoising the object!5 const theme = useMemo(() => ({ mode }), [mode]);67 return (8 <ThemeContext.Provider value={theme}>9 // same10 </ThemeContext.Provider>11 );12};
And now the counter will work without causing all the Items to re-render!
And absolutely the same solution for preventing unnecessary re-renders we can apply to our non-simplified Page
component:
1export const Page = ({ countries }: { countries: Country[] }) => {2 // same as before3 const [mode, setMode] = useState<Mode>("light");45 // memoising the object!6 const theme = useMemo(() => ({ mode }), [mode]);78 return (9 <ThemeContext.Provider value={theme}>10 <button onClick={() => setMode(mode === 'light' ? 'dark' : 'light')}>Toggle theme</button>11 // the rest is the same as before12 </ThemeContext.Provider>13 )14}
And extract the new knowledge into the final rule of this article:
Bringing it all together
And finally, our app is complete! The entire implementation is available in this codesandbox. Throttle your CPU if you’re on the latest MacBook, to experience the world as the usual customers are, and try to select between different countries on the list. Even with 6x CPU reduction, it’s still blazing fast! 🎉
And now, the big question that I suspect many people have the urge to ask: “But Nadia, React is blazing fast by itself anyway. Surely those ‘optimisations’ that you did won’t make much of a difference on a simple list of just 250 items? Aren’t you exaggerating the importance here?“.
Yeah, when I just started this article, I also thought so. But then I implemented that app in the “non-performant” way. Check it out in the codesandbox. I don’t even need to reduce the CPU to see the delay between selecting the items 😱. Reduce it by 6x, and it’s probably the slowest simple list on the planet that doesn’t even work properly (it has a focus bug that the “performant” app doesn’t have). And I haven’t even done anything outrageously and obviously evil there! 😅
So let’s refresh when React components re-render:
- when props or state have changed
- when parent component re-renders
- when a component uses context and the value of its provider changes
And the rules we extracted:
Rule #1: If the only reason why you want to extract your inline functions in props into useCallback
is to avoid re-renders of children components: don’t. It doesn’t work.
Rule #2: If your component manages state, find parts of the render tree that don’t depend on the changed state and memoise them to minimize their re-renders.
Rule #3. Never create new components inside the render function of another component.
Rule #4. When using context, make sure that value
property is always memoised if it’s not a number, string or boolean.
That is it! Hope those rules will help to write more performant apps from the get-go and lead to happier customers who never had to experience slow products anymore.
Bonus: the useCallback
conundrum
I feel I need to resolve one mystery before I actually end this article: how can it be possible that useCallback
is useless for reducing re-renders, and why then React docs literally say that “[useCallback] is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders”? 🤯
The answer is in this phrase: “optimized child components that rely on reference equality”.
There are 2 scenarios applicable here.
First: the component that received the callback is wrapped in React.memo
and has that callback as a dependency. Basically this:
1const MemoisedItem = React.memo(Item);23const List = () => {4 // this HAS TO be memoised, otherwise `React.memo` for the Item is useless5 const onClick = () => {console.log('click!')};67 return <MemoisedItem onClick={onClick} country="Austria" />8}
or this:
1const MemoisedItem = React.memo(Item, (prev, next) => prev.onClick !== next.onClick);23const List = () => {4 // this HAS TO be memoised, otherwise `React.memo` for the Item is useless5 const onClick = () => {console.log('click!')};67 return <MemoisedItem onClick={onClick} country="Austria" />8}
Second: if the component that received the callback has this callback as a dependency in hooks like useMemo
, useCallback
or useEffect
.
1const Item = ({ onClick }) => {2 useEffect(() => {3 // some heavy calculation here4 const data = ...5 onClick(data);67 // if onClick is not memoised, this will be triggered on every single render8 }, [onClick])9 return <div>something</div>10}11const List = () => {12 // this HAS TO be memoised, otherwise `useEffect` in Item above13 // will be triggered on every single re-render14 const onClick = () => {console.log('click!')};1516 return <Item onClick={onClick} country="Austria" />17}
None of this can be generalised into a simple “do” or “don’t do”, it can only be used for solving the exact performance problem of the exact component, and not before.
And now the article is finally done, thank you for reading it so far and hope you found it useful! Bleib gesund and see ya next time ✌🏼