Some best practices for managing and optimizing React components
Hard coded values
Let's start talking about constant values or hard-coded values. We usually have default values that are hard coded (they don't change) and needed in different files or components.
In our example we may have a value that indicates what is the maximum number of todos before the user is asked to login in order to add more. What if we change our mind in the future and allow our users more todos?
We could also have a list of words that we know might be sensitive information and let the user know we can't process the request.
What we typically do in these scenarios is create a separate variable outside our component or file to store these values, this helps us to globally change a value without needing to find it in multiple files or lines of code. A common standard is to uppercase the word in Snake CaseExternal Link.
Lastly if we have initial constant data in an array or object inside the component, that data will be recreated on every render, for a small array or object it might not be a problem but it is something to keep in mind. A better solution will be to move the constant values outside the component and that also helps keep the component clean.
export const MAX_FREE_TIER = 5; export const SENSITIVE_WORDS = ['password123', 'credit card', 'admin'];
@/app/lib/constants.ts
'use client'; // used only for nextjs import React, { useState } from 'react'; import { MAX_FREE_TIER, SENSITIVE_WORDS } from '@/app/lib/constants'; type Todo = { id: number; content: string; completed: boolean; }; // const MAX_DATA = 10; const initialData = [ { id: 1, content: 'Check car oil', completed: false, }, { id: 2, content: 'Check account balance', completed: true, }, ]; export default function HardCodedValues() { const [todos, setTodos] = useState<Todo[]>(initialData); const [content, setContent] = useState(''); return ( <section> {todos.map((todo) => ( <div key={todo.id}>{todo.content}</div> ))} <form onSubmit={(e) => { e.preventDefault(); if (todos.length === MAX_FREE_TIER) { console.log(`You need to sign in to add more than ${MAX_FREE_TIER} todos`); return; } if (SENSITIVE_WORDS.includes(content)) { console.log('Cannot save sensitive information'); return; } setTodos((prev) => [ ...prev, { id: prev.length + 1, content: content, completed: false }, ]); setContent(''); }}> <input name='content' placeholder='Type here' value={content} onChange={(event) => setContent(event.target.value)} /> <button type='submit' className='py-2 px-4'> Submit </button> </form> </section> ); }
tsx
Layout styles in reusable components
Try not to add layout styles to reusable components.
In our example we have a header component that is used in multiple places. This component might have a problem with spacing in some places (it might appear too close to other content).
A naive solution at first might be to write layout styles (e.g. margin, flexbox, grids) in the reusable component, but that could cause more problems with the rest of the content.
A second solution could be to wrap the component in a single div and apply styles there to space the header from the other content.
import React from 'react'; type HeaderProps = { children: React.ReactNode; }; const Header = ({ children }: HeaderProps) => { return <h1 className='text-3xl lg:text-6xl font-bold tracking-tight'>{children}</h1>; }; export default function LayoutInReusableComponents() { return ( <section className='flex flex-col items-center'> <div className='mb-4'> <Header>This is a nice header</Header> </div> <ul> <li>Task 1: Water the plants</li> <li>Task 2: Buy groceries</li> <li>Task 3: Finish reading the book</li> </ul> </section> ); }
tsx
And another solution is to just accept an optional class prop inside our headers component and maintain all the other base classes intact.
import React from 'react'; type HeaderProps = { children: React.ReactNode; className?: string; }; const Header = ({ children, className }: HeaderProps) => { return ( <h1 className={`text-3xl lg:text-6xl font-bold tracking-tight ${className}`}> {children} </h1> ); }; export default function LayoutInReusableComponents() { return ( <section className='flex flex-col items-center'> <Header className='mb-10'>This is a nice header</Header> <ul> <li>Task 1: Water the plants</li> <li>Task 2: Buy groceries</li> <li>Task 3: Finish reading the book</li> <li>Task 4: Call Mom</li> <li>Task 5: Schedule a dentist appointment</li> </ul> </section> ); }
tsx
Keeping reusable components simple
Imagine we have a parent component that manages a list of products and we want to display the total price of all the products.
We create a child component ProductSummary
that summarizes the total price of the products.
'use client'; // used only for nextjs import React, { useState } from 'react'; type Product = { id: number; name: string; price: number; }; function ProductSummary({ products }: { products: Product[] }) { // Derived state: Calculate the total price of all products const totalPrice = products.reduce((total, product) => total + product.price, 0); return ( <> <h3>Summary</h3> <p>Total Products: {products.length}</p> <p>Total Price: ${totalPrice}</p> </> ); } export default function ProductList() { const [products] = useState([ { id: 1, name: 'Laptop', price: 999 }, { id: 2, name: 'Smartphone', price: 499 }, { id: 3, name: 'Tablet', price: 299 }, ]); return ( <section> <h2>Product List</h2> <ul> {products.map((product) => ( <li key={product.id}> {product.name} - ${product.price} </li> ))} </ul> {/* Passing the entire list of products to the child component */} <ProductSummary products={products} /> </section> ); }
tsx
Now, what could possibly be a bad practice in this case?
For one, if the summary gets state that causes a rerender, the total price will be recalculated even if it's not necessary.
Another problem could be that now the child component is now tightly coupled with the parent structure and the products array. If we wanted to reuse the component in a different page or component we would need to change the logic of the child component making it harder to maintain.
A better approach will be to calculate the summary in the parent component. The child component now only needs to display the data it receives, making it simpler and more focused on presentation.
'use client'; // used only for nextjs import React, { useState } from 'react'; function ProductSummary({ totalPrice, productCount, }: { totalPrice: number; productCount: number; }) { return ( <> <h3>Summary</h3> <p>Total Products: {productCount}</p> <p>Total Price: ${totalPrice}</p> </> ); } export default function ProductList() { const [products] = useState([ { id: 1, name: 'Laptop', price: 999 }, { id: 2, name: 'Smartphone', price: 499 }, { id: 3, name: 'Tablet', price: 299 }, ]); // Calculate total price in the parent component const totalPrice = products.reduce((total, product) => total + product.price, 0); return ( <section> <h2>Product List</h2> <ul> {products.map((product) => ( <li key={product.id}> {product.name} - ${product.price} </li> ))} </ul> {/* Pass the calculated total price as a prop */} <ProductSummary totalPrice={totalPrice} productCount={products.length} /> </section> ); }
tsx
This is just one of many use cases, sometimes having a derived state in a child component is fine. A child component can have its own logic and might be easier to reason about its behavior without needing to consider the parent's state or logic.
A parent component might have a lot of states to manage and a child component will help handle specific logic and lead to cleaner code separation.
Children Prop pattern
Lets analyze the next example:
import React from 'react'; const Grandchild = ({ message }: { message: string }) => { return <div>{message}</div>; }; const Child = ({ message }: { message: string }) => { return ( <div className="flex flex-col"> <h1>This is a child component</h1> <Grandchild message={message} /> </div> ); }; export default function Container() { return ( <section className='container'> <Child message='Hello world!'></Child> </section> ); }
tsx
In this example we can see that the Grandchild component does not depend on any local variables inside the Child component And the message prop is just passed down and the Child component doesn't need it.
The children pattern allows us to pass components or data directly from a parent component to intermediate components without them needing to know about it.
The children
prop is a special prop in React. It automatically populates the content between the opening and closing tags of the component.
Here's how you can refactor the above example:
'use client'; // used only for nextjs import React from 'react'; const Grandchild = ({ message }: { message: string }) => { console.log('Grandchild re-render'); return <div>{message}</div>; }; const Child = ({ children }: { children: React.ReactNode }) => { const [count, setCount] = React.useState(0); console.log('Child re-render'); return ( <div className='flex flex-col'> <h1>This is a child component</h1> <p>{count}</p> <button onClick={() => setCount(count + 1)}>Click me</button> {children} </div> ); }; export default function Container() { return ( <section className='container'> <Child> <Grandchild message='Hello world!' /> </Child> </section> ); }
tsx
This pattern reduces the need to pass props through multiple layers, making the code easier to maintain and the components more reusable.
It also comes with the benefit of performance, in our example Grandchild will not rerender when the Child component rerenders because it is not part of the Child's component tree. Grandchild belongs to the Container component.
We have to keep in mind that this pattern only works when the children's elements do not depend on their parents' internal variables.
A small downside might be that the container component becomes more bloated.
Memoization
Everytime components rerender when state/props change, depending on how complicated or if an expensive computation is needed, that rerender might be slow.
To help us with this situation React provides us useMemo
, useCallback
and React.memo
Imagine we have a component showing users, we might have hundreds of users to show, so every time the users change, a rerender will be triggered and also all the component statements will run again.
'use client'; // used only for nextjs import React, { useState } from 'react'; const allUsers = [ { id: 1, active: true, name: 'Maria' }, { id: 2, active: true, name: 'John' }, { id: 3, active: true, name: 'Nataly' }, ]; const ToggleUserStatus = ({ toggleUser }: { toggleUser: () => void }) => { return ( <button className='px-4 py-2 bg-orange-400' onClick={toggleUser}> Toggle user </button> ); }; type User = { id: number; active: boolean; name: string; }; const ShowUsers = ({ users, activeUsers }: { users: User[]; activeUsers: number }) => { return ( <> <p>Active users: {activeUsers.toFixed(1)}%</p> {users.map((user) => ( <p key={user.id}> {user.name}: {user.active + ''} </p> ))} </> ); }; export default function Memoization() { const [count, setCount] = useState(0); const [users, setUsers] = useState(allUsers); // This computation will run on every rerender const activeUsers = (users.filter((user) => user.active).length / users.length) * 100; // A new function is created and assigned to this variable on each rerender const toggleUser = () => { const randomId = Math.floor(Math.random() * users.length); const updatedUser = users.map((user, index) => index === randomId ? { ...user, active: !user.active } : user ); setUsers(updatedUser); }; return ( <section> <p>count: {count}</p> <button className='px-4 py-2 bg-slate-500' onClick={() => setCount((prev) => prev + 1)}> Increment {count} </button> <ToggleUserStatus toggleUser={toggleUser} /> <ShowUsers users={users} activeUsers={activeUsers} /> </section> ); }
tsx
So to avoid expensive computations from running everytime we can use the useMemo
hook. It is often used for calculations, objects arrays or derived states (values derived from existing state).
We can also use the useCallback
hook for functions, useCallback
is syntactic sugar. It exists purely to make our lives a bit nicer when trying to memoize callback functions.
In our example a new function is created and assigned to a variable on each rerender but by wrapping it in useMemo the function will be cached.
And at last we can also memoize components using memo
, if props do not change, the component will be cached and not rerender. In our example since we are using useCallback
on the toggleUser function, the function will remain the same between rerenders and therefore the ToggleUserStatus component will stay the same.
'use client'; import React, { memo, useCallback, useMemo, useState } from 'react'; const allUsers = [ { id: 1, active: true, name: 'Maria' }, { id: 2, active: true, name: 'John' }, { id: 3, active: true, name: 'Nataly' }, ]; const ToggleUserStatus = memo(function ToggleUser({ toggleUser, }: { toggleUser: () => void; }) { return ( <button className='px-4 py-2 bg-orange-400' onClick={toggleUser}> Toggle user </button> ); }); type User = { id: number; active: boolean; name: string; }; const ShowUsers = memo(function ShowUsers({ users, activeUsers, }: { users: User[]; activeUsers: number; }) { return ( <> <p>Active users: {activeUsers.toFixed(1)}%</p> {users.map((user) => ( <p key={user.id}> {user.name}: {user.active + ''} </p> ))} </> ); }); export default function Memoization() { const [count, setCount] = useState(0); const [users, setUsers] = useState(allUsers); // This computation will run on every rerender without useMemo const activeUsers = useMemo( () => (users.filter((user) => user.active).length / users.length) * 100, [users] ); // A new function is created and assigned to this variable on each rerender without useCallback const toggleUser = useCallback(() => { const randomId = Math.floor(Math.random() * users.length); const updatedUser = users.map((user, index) => index === randomId ? { ...user, active: !user.active } : user ); setUsers(updatedUser); }, [users]); return ( <section> <p>count: {count}</p> <button className='px-4 py-2 bg-slate-500' onClick={() => setCount((prev) => prev + 1)}> Increment {count} </button> <ToggleUserStatus toggleUser={toggleUser} /> <ShowUsers users={users} activeUsers={activeUsers} /> </section> ); }
tsx
When clicking on the increment button on the parent component, it doesn't trigger a rerender in the two child components
Note: Using these hooks could change in the future. The React team is actively investigating whether it's possible to “auto-memoize” code during the compile step. It's still in the research phase.
useState updater function
If we have a state that is changing and it depends on the previous value we should use the functional way of setting state. For example:
setUsers((prev) => [...prev, { id: 4, active: true, name: "Mike" }]);
tsx
We want to keep the previous values by spreading them and add or update the new value.
Doing it like this we avoid problems with old values being rendered instead of the expected new ones.
'use client'; import React, { useEffect, useState } from 'react'; const allUsers = [ { id: 1, active: true, name: 'Maria' }, { id: 2, active: true, name: 'John' }, { id: 3, active: true, name: 'Nataly' }, ]; export default function SetterFunction() { const [count, setCount] = useState(0); const [users, setUsers] = useState(allUsers); useEffect(() => { setUsers((prev) => [...prev, { id: 4, active: true, name: 'Mike' }]); const intervalId = setInterval(() => { setCount((prev) => prev + 1); // Doing it like this will not work // setCount(count + 1); // setCount(count + 1); // setCount(count + 1); }, 1000); return () => { clearInterval(intervalId); }; }, []); const handleClick = () => { setCount((prev) => prev + 1); setCount((prev) => prev + 1); }; const handleClickAsync = () => { setTimeout(() => { setCount((prev) => prev + 1); }, 1000); }; return ( <section> <p>{count}</p> <div> {users.map((user) => ( <p key={user.id}>{user.name}</p> ))} </div> <button onClick={handleClick} className='bg-blue-500 px-4 py-2 text-white rounded'> Add two more </button> <button onClick={handleClickAsync} className='bg-blue-500 px-4 py-2 text-white rounded'> Add number after 1 sec </button> </section> ); }
tsx
Single State
Instead of having multiple states for closely related features or logic consider using a single state.
In our example if we have a loading, error and success states, they can be united into a single state.
'use client'; import React, { useState } from 'react'; type Status = 'init' | 'loading' | 'error' | 'success'; export default function UnionTypes() { const [status, setStatus] = useState<Status>('init'); return <section>{status}</section>; }
tsx
One source of Truth
Keep track of active or selected items by its id and not by copying the whole item.
We might be tempted to copy a whole item when selecting or toggling an item in a list. But if that item changes in the future we will have two different sources of truth. They will be out of sync and we will need to update both states to keep them in sync.
If we need the whole item we can just derive state and get the whole object.
'use client'; import React, { useState } from 'react'; type User = { id: number; name: string; }; export default function SingleTruth() { const [users, setUsers] = useState([ { id: 1, name: 'Nataly' }, { id: 2, name: 'John' }, { id: 3, name: 'Alice' }, ]); const [activeUser, setActiveUser] = useState<number | null>(null); const selectedUser = users.find((user) => user.id === activeUser); const handleSelectUser = (user: User) => { setActiveUser(user.id); }; return ( <div> <h2>Select a User</h2> {users.map((user) => ( <button className='bg-slate-500 px-4 py-2 m-1' key={user.id} onClick={() => handleSelectUser(user)}> {user.name} </button> ))} {selectedUser ? ( <div> <h3>Active User:</h3> <p>{selectedUser.name}</p> </div> ) : ( <div> <h3>No user selected</h3> </div> )} </div> ); }
tsx
Use Url params instead of state
When we have information like filters, variants, or pagination, putting the information in the url may be a better option, we can even share the url and get the exact same view we were looking at. Besides sharing, people could also bookmark the page and come back to the same view.
In our example with Next.jsExternal Link we are setting a url param using an input, we could later use this information to filter a table.
'use client'; import React from 'react'; import { useSearchParams, usePathname, useRouter } from 'next/navigation'; export default function Search() { const searchParams = useSearchParams(); const pathname = usePathname(); const { replace } = useRouter(); function handleSearch(term: string) { const params = new URLSearchParams(searchParams); if (term) { params.set('query', term); } else { params.delete('query'); } replace(`${pathname}?${params.toString()}`); } return ( <input placeholder={'search'} onChange={(e) => { handleSearch(e.target.value); }} defaultValue={searchParams.get('query')?.toString()} /> ); }
tsx