Top 25 React Tips Every Developer Should Know - Part 1

Yogesh Chavan - Oct 17 - - Dev Community

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>
};
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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];
};
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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} />
))}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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);
}, []);
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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);
}, []);
Enter fullscreen mode Exit fullscreen mode

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>;
Enter fullscreen mode Exit fullscreen mode

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}`;
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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([]);
Enter fullscreen mode Exit fullscreen mode

Never declare any state without any default value like this:

const [pageNumbers, setPageNumbers] = useState();
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

instead of an empty object like this:

const [user, setUser] = useState({});
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

or use JSX expression like this:

{ user && <p>User found</p>}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

or use JSX expression like this:

{ Object.keys(user).length === 0 && <p>User found</p>}
Enter fullscreen mode Exit fullscreen mode

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>
 );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

and use it as shown below:

<AuthContextProvider>
  <ComponentA />
  <ComponentB />
</AuthContextProvider>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 /> }
Enter fullscreen mode Exit fullscreen mode

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 /> }
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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() }
Enter fullscreen mode Exit fullscreen mode

Thanks for Reading!

Want to stay up to date with regular content regarding JavaScript, React, and Node.js? Follow me on LinkedIn.

Get Lifetime/Pro Subscription

Master JavaScript, React and Node.js

Subscribe to my YouTube Channel

My GitHub

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .