Whether you’re already familiar with React or building your way up from the basics, there’s always room to improve your workflow and code efficiency.
Here are 25 React tips to help you write better React code.
1. Avoid declaring/nesting Components Inside Other Components
While it may seem convenient at first, this practice can cause major performance issues in your application.
Every time the parent component is re-rendered because of a state update, the nested component is re-created, which means the previous state and data declared in that nested component are lost causing major performance issues.
// Wrong Way:
const UsersList = () => {
❌ Declaring User component inside UsersList component
const User = ({ user }) => {
// some code
return <div>{/* User Component JSX */}</div>
};
return <div>{/* UsersList JSX */}</div>
};
// Right Way:
✅ Declaring User component outside UsersList component
const User = ({ user }) => {
// some code
return <div>{/* User Component JSX */}</div>
};
const UsersList = ({ users }) => {
// some code
return <div>{/* UsersList component JSX */}</div>
};
2. Use the Dependency Array In useEffect
Correctly
Always include every outside variable referenced inside useEffect
in the dependency array []
to avoid bugs caused by stale closures.
useEffect(() => {
const fetchData = (id) => {
// some code to fetch data
}
id && fetchData(id);
}, [id]);
3. Use Lazy Initialization of State with useState
For expensive initial state computations, you can delay the evaluation until the component is rendered by passing a function to useState
.
This will ensure the function only runs once during the initial render of the component. Check out the application that uses useLocalStorage
hook.
const useLocalStorage = (key, initialValue) => {
const [value, setValue] = useState(() => {
// lazy initialization of state
try {
const localValue = window.localStorage.getItem(key);
return localValue ? JSON.parse(localValue) : initialValue;
} catch (error) {
return initialValue;
}
});
useEffect(() => {
window.localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
};
4. Memoize Expensive Functions with useMemo
For expensive computations that don’t need to run on every render, use useMemo
hook to memoize the result and improve performance.
The first code shown below causes a performance issue because, on every re-render of the component, the users
array is filtered and sorted again and again.
And If the users
array contains a lot of data, then it will slow down the application.
But If we use the useMemo
hook, the users
array will be filtered and sorted only when the users
dependency is changed.
// Instead of writing code like this:
function App({ users }) {
return (
<ul>
{users
.map((user) => ({
id: user.id,
name: user.name,
age: user.age,
}))
.sort((a, b) => a.age - b.age)
.map(({ id, name, age }) => {
return (
<li key={id}>
{name} - {age}
</li>
);
})}
</ul>
);
}
// write it like this:
function App({ users }) {
const sortedUsers = useMemo(() => {
return users
.map((user) => ({
id: user.id,
name: user.name,
age: user.age,
}))
.sort((a, b) => a.age - b.age);
}, [users]);
return (
<ul>
{sortedUsers.map(({ id, name, age }) => {
return (
<li key={id}>
{name} - {age}
</li>
);
})}
</ul>
);
}
5. Use Custom Hooks for Reusable Logic
Custom hooks are a powerful way to extract reusable logic from your components. If you find yourself copying and pasting the same code in multiple components, create a custom hook.
function useFetch(url) { // custom hook to get data from API by passing url
const [data, setData] = useState(null);
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(data => setData(data));
}, [url]);
return data;
}
6. Use Fragments to Avoid Extra DOM Nodes
Wrapping components with unnecessary <div>
elements can lead to a bloated DOM. Use fragments instead.
This keeps your DOM clean and optimized for performance.
Note that, every time you add a new <div>
, React has to create that element, using React.createElement
so avoid creating unnecessary <div>
.
// Instead of writing like this:
return (
<div>
<Child1 />
<Child2 />
</div>
);
// write like this:
return (
<>
<Child1 />
<Child2 />
</>
);
// or like this:
return (
<React.Fragment>
<Child1 />
<Child2 />
</React.Fragment>
);
7. Leverage Compound Components for Flexible UIs
Compound components let you create flexible UIs by allowing components to communicate implicitly.
It’s a great way of building reusable and highly customizable components.
function Tab({ children }) {
return <div>{children}</div>;
}
Tab.Item = function TabItem({ label, children }) {
return (
<div>
<h3>{label}</h3>
<div>{children}</div>
</div>
);
};
// Usage
<Tab>
<Tab.Item label="Tab 1">Content 1</Tab.Item>
<Tab.Item label="Tab 2">Content 2</Tab.Item>
</Tab>
8. Always Add key
Prop While Rendering Using map Method
Always add a unique key
prop to items rendered in a list to help React optimize rendering.
The value of
key
should remain the same and should not change on component re-render.
Always have a unique ID for each element of the array which can be used as a key.
Avoid using an array index
as a value for key
.
Using an array index as key
prop can lead to unexpected UI behavior when array elements are added, removed, or reordered.
// ✅ Use unique id as a key
{items.map(item => (
<Item key={item.id} item={item} />
))}
// ❌ Don't use index as the key
{items.map((item, index) => (
<Item key={index} item={item} />
))}
9. Use Error Boundary To Avoid Breaking Your Application On Production
React Error Boundaries provide a powerful mechanism to catch and gracefully handle errors that occur during the rendering of your components.
They help prevent your entire application from crashing by isolating errors to specific components, allowing the rest of your app to continue functioning.
<ErrorBoundary
FallbackComponent={ErrorPage}
onReset={() => (location.href = '/')}
>
<App />
</ErrorBoundary>
10. Avoid Prop Drilling by Using Context
Instead of passing props through many nested components, use React Context API for more manageable data sharing.
Best Use Case: Sharing global state like theme, user data, or settings.
export const AuthContext = React.createContext();
export const AuthContextProvider = ({ children }) => {
const [user, setUser] = useState(null);
const updateUser = (user) => {
setUser(user);
};
return (
<AuthContext.Provider value={{ user, updateUser }}>
{children}
</AuthContext.Provider>
);
};
// Usage
<AuthContextProvider>
<App />
</AuthContextProvider>
11. Use useRef
hook to Persist Value Across Renders
Instead of using the useState
hook to store value, use the useRef
hook, If you don’t want to re-render the component on setting value.
Use Case: Storing timeouts, intervals, DOM references, or any value that doesn’t need to trigger re-renders, to avoid closure issues.
const interval = useRef(-1);
useEffect(() => {
interval.current = setInterval(() => {
console.log('Timer executed');
}, 1000);
return () => clearInterval(interval.current);
}, []);
12. Use React Suspense for Code Splitting
React’s Suspense lets you split your code into smaller chunks, improving performance by loading components only when needed.
import { Suspense, lazy } from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
const Dashboard = lazy(() => import('./pages/dashboard'));
const Profile = lazy(() => import('./pages/profile'));
const App = () => {
return (
<BrowserRouter>
<Suspense fallback={<p>Loading...</p>}>
<Routes>
<Route path='/dashboard' element={<Dashboard />} />
<Route path='/profile' element={<Profile />} />
</Routes>
</Suspense>
</BrowserRouter>
);
};
In the above code, Profile
and Dashboard
component code will not be loaded initially, instead, it will be loaded only when /profile
route or /dashboard
route is accessed respectively.
13. Use StrictMode
for Highlighting Potential Issues
Enable React’s StrictMode
in development to identify potential problems in your app like unsafe lifecycles, deprecated methods, or unexpected side effects.
Enabling StrictMode
shows a warning in the browser console which helps to avoid future bugs.
<React.StrictMode>
<App />
</React.StrictMode>
14. Manage State Locally Before Lifting It Up
Avoid lifting state to parent components unnecessarily.
If a piece of state is only used by a single component or closely related components, keep it local to avoid over-complicating the state management.
Because whenever the state in the parent component changes, all the direct as well as indirect child components get re-rendered.
function ChildComponent() {
const [value, setValue] = useState(0);
return <div>{value}</div>;
}
15. Prefer Functional Components over Class Components
Always use functional components with hooks instead of class components.
Functional components offer more concise syntax, and better readability, and make it easier to manage side effects.
They’re also a bit faster than class components because they don’t have inherited life cycle methods, and extra code of parent inherited classes
function MyComponent() {
const [state, setState] = useState(0);
return <div>{state}</div>;
}
16. Use IntersectionObserver for Lazy Loading Elements
Use Intersection Observer API to load images, videos, or other elements only when they’re visible on the screen.
This improves performance, especially in long lists or media-heavy apps.
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// Load image or other data
}
});
});
observer.observe(elementRef.current);
}, []);
17. Split Large Components into Smaller Ones
Break down large components into smaller, reusable components for improved readability and maintainability.
Aim for components that follow the “single responsibility” principle.
Smaller components are easier to test, maintain, and understand.
const Header = () => <header>{/* Header Component Code */}</header>;
const Footer = () => <footer>{/* Footer Component Code */}</footer>;
18. Avoid Unnecessary useEffect and setState Calls
Keep in mind that the code written in the useEffect
hook will always execute after the render cycle, and calling setState
inside useEffect
triggers a re-render of your component again.
Additionally, if the parent component re-renders, all child components will also re-render. To avoid unnecessary re-renders, always aim to minimize extra useEffect
hooks and avoid redundant setState
calls when possible.
// Instead of writing code like this:
const [username, setUsername] = useState("");
const [age, setAge] = useState("");
const [displayText, setDisplayText] = useState("");
useEffect(() => {
setDisplayText(`Hello ${username}, your year of birth is ${new Date().getFullYear() - age}` );
}, [username, age]);
// write it like this:
const [username, setUsername] = useState("");
const [age, setAge] = useState("");
// create local variable in component
const displayText = `Hello ${username},
your year of birth is ${new Date().getFullYear() - age}`;
With the above code, the displayText
will always be updated when the username
or age
is changed and it avoids extra re-renders of your component.
19. Avoid Writing Extra Functions In Components If Possible
Every function declared inside a functional component gets recreated on every re-render of the component.
So, If you have declared separate functions for showing/hiding a modal that manipulates the state like this:
const [isOpen, setIsOpen] = React.useState(false);
function openModal() {
setIsOpen(true);
}
function closeModal() {
setIsOpen(false);
}
Then, you can simplify this logic by creating a single toggleModal
function that handles both opening and closing the modal.
const [isOpen, setIsOpen] = React.useState(false);
function toggleModal() {
setIsOpen((open) => !open);
}
So, by just calling the toggleModal
function, the modal will be closed, If it’s open and vice-versa.
20. Always Assign Initial Value For useState Hook
Whenever you’re declaring a state in a functional component, always assign an initial value like this:
// set initial value of an empty array
const [pageNumbers, setPageNumbers] = useState([]);
Never declare any state without any default value like this:
const [pageNumbers, setPageNumbers] = useState();
It makes it difficult to understand what value to assign when you have a lot of states and you’re trying to set the state using the setPageNumbers
function later in the code.
If you’re working in a team, one developer might assign an array in place using setPageNumbers([1, 2, 3, 4])
and another might assign a number like setPageNumbers(20)
.
If you’re not using TypeScript, it might create a hard-to-debug issue which is caused just because of not specifying an initial value.
So always specify the initial value while declaring a state.
21. Understanding State Updates and Immediate Access in React
In React, it’s important to remember that the state does not get updated immediately after setting it. This means that if you try to access the state value immediately after setting it, you might not get the updated value.
For instance, consider the following code where we attempt to log the updated state right immediately after calling setIsOnline
.
const [isOnline, setIsOnline] = React.useState(false);
function toggleOnlineStatus() {
setIsOnline(!isOnline);
console.log(isOnline); // Logs the old value, not the updated one
}
The same happens with the below code also:
const [isOnline, setIsOnline] = React.useState(false);
function doSomething() {
if(isOnline){ // isOnline value is still old and not updated yet
// do something
}
}
function toggleOnlineStatus() {
setIsOnline(!isOnline);
doSomething();
}
React updates state during the next render, after it has done executing all the code from the current function.
So in the above doSomething
function, isOnline
will still have the old value and not the updated value.
22. Avoid Using Context API In Every Case
Passing props down to components one or two levels deep is not an issue. Props are designed to be passed to components, so there’s no need to use the Context API just to avoid passing those props.
Many React developers use the Context API simply to avoid passing props, but that’s not the intended use case. The Context API should be used only when you want to avoid prop drilling, which refers to passing props to deeply nested components.
23. Always Use null
As The Value To Store Objects In State
Whenever you want to store an object in a state, use null
as the initial value like this:
const [user, setUser] = useState(null);
instead of an empty object like this:
const [user, setUser] = useState({});
So if you want to check if a user
exists or if there are any properties present inside it, you can just use a simple if condition like this:
if (user) {
// do something
}
or use JSX expression like this:
{ user && <p>User found</p>}
However, If you use an empty object as the initial value, then to find out If user
exists or not, you need to check like this:
if(Object.keys(user).length === 0) {
// do something
}
or use JSX expression like this:
{ Object.keys(user).length === 0 && <p>User found</p>}
24. Always Create ContextProvider
Component Instead Of Directly Using Context.Provider
Whenever using React Context API, don’t pass value
prop directly to Provider
as shown below:
const App = () => {
....
return (
<AuthContext.Provider value={{ user, updateUser }}>
<ComponentA />
<ComponentB />
</AuthContext.Provider>
);
}
In the above code, whenever the App component re-renders because of state update, the value
prop passed to the AuthContext.Provider
will change,
and because of that, all the direct as well as indirect components in between the opening and closing tag of AuthContext.Provider
will get re-rendered unnecessarily.
So, always create a separate component as shown below:
export const AuthContextProvider = ({ children }) => {
const [user, setUser] = useState(null);
const updateUser = (user) => {
setUser(user);
};
return (
<AuthContext.Provider value={{ user, updateUser }}>
{children}
</AuthContext.Provider>
);
};
and use it as shown below:
<AuthContextProvider>
<ComponentA />
<ComponentB />
</AuthContextProvider>
In React, children
is a special prop, so even if the parent component re-renders, any component passed as a children
prop will not get re-rendered.
So when you write code like this:
<AuthContextProvider>
<ComponentA />
<ComponentB />
</AuthContextProvider>
then the components wrapped in between the opening and closing tag of AuthContextProvider
will only get re-rendered if they’re using the useContext
hook, otherwise, they will not get re-rendered even if the parent component re-renders.
So to avoid unnecessary re-rendering of all child components, always create separate component to wrap the children
prop.
25. Avoid Unintended Rendering with Logical AND (&&) in JSX
Don’t make the mistake of writing JSX like this:
{ users.length && <User /> }
In the above code, If the users.length
is zero then we will end up displaying 0
on the screen.
Because the logical &&
operator(also known as short circuit operator) instantly returns that value if it evaluates to a falsy value.
So the above code will be equivalent to the below code if users.length
is 0
.
{ 0 && <User /> }
which evaluates to {0}
Instead, you can use either of the following ways to correctly write the JSX:
→ { users.length > 0 && <User /> }
→ { !!users.length && <User /> }
→ { Boolean(users.length) && <User /> }
→ { users.length ? <User /> : null }
I usually use the first approach of checking users.length > 0
.
However, you can use any of the above ways.
The !!
operator converts any value into its boolean true
or false
.
So the above 2nd and 3rd conditions are equivalent.
The ternary operator ?
in the fourth case above will check for the truthy or falsy value of the left side of ?
and as 0
is falsy value, so null
will be returned.
And as you might know, React will not display null
, undefined
and boolean values on the UI when used in JSX expression like this:
{ null }
{ undefined }
{ true }
{ false }
If you want the boolean values to display on screen, you need to convert it into a string like this:
const isLoggedIn = true;
{ "" + isLoggedIn }
OR
{ `${isLoggedIn}` }
OR
{ isLoggedIn.toString() }
Thanks for Reading!
Want to stay up to date with regular content regarding JavaScript, React, and Node.js? Follow me on LinkedIn.
Master JavaScript, React and Node.js