The Talent500 Blog
react

Anti-patterns in React that You Should Avoid

React is one of the most popular JavaScript frontend libraries, with about 22 million downloads every week. It’s been ruling web development for over a decade and is trusted by big names like Twitter, Instagram, and Airbnb.

React also recently released the React 19 beta version, which includes various new features.

Before building a production-ready React application, it’s important to avoid several anti-patterns while writing React. In this detailed blog, we will explore some of these anti-patterns in React and discuss how to write better code.

Without further ado, let’s get started

Props Drilling

One of the common errors that new React developers make is prop drilling. It is the process of passing props from the parent component to its child component.

Let’s take a look at this example:

import React, { useState } from “react”;

const Child = ({ increment }) => {

  return (

    <div>

      <button onClick={increment}>Increment</button>

    </div>

  );

};

const Parent = ({ increment }) => {

  return (

    <div>

      <Child increment={increment} />

    </div>

  );

};

const Grandparent = ({ increment }) => {

  return (

    <div>

      <Parent increment={increment} />

    </div>

  );

};

const App = () => {

  const [count, setCount] = useState(0);

  const increment = () => {

    setCount(count + 1);

  };

  return (

    <div>

      <Grandparent increment={increment} />

      <div>Count: {count}</div>

    </div>

  );

};

export default App;

Here you can see that the increment function is passed down through multiple layers of components just to be used in the Child component. If the application grows in size or there are more components between App and Child, this prop drilling pattern will result in a lot of redundant code, making it more difficult to maintain.

Prop drilling becomes a problem when you have to pass props through several intermediary components that don’t actually use the props themselves, but just serve as messengers to pass them down further. This can make the code harder to maintain and understand especially as the application grows larger.

To reduce prop drilling, you can use techniques like React’s Context API or state management libraries such as Redux. These tools allow you to manage and access global data without having to pass props manually through every level of the component tree.

Using Index as Key

Using the index of an array as a key in React is a common anti-pattern that developers should avoid.  

React uses keys to identify elements in a list and then track changes efficiently during updates. When you use the index as the key, React doesn’t have a stable identifier for all the items. If the order of the items changes or if items are added or removed, React may re-render more items than necessary, leading to potential performance issues.

Bad approach

import React from ‘react’;

const ListItems = () => {

  const items = [‘Apple’, ‘Banana’, ‘Orange’];

  return (

    <ul>

      {items.map((item, index) => (

        <li key={index}>{item}</li>

      ))}

    </ul>

  );

};

export default ListItems;

Here we are using the index of each item in the array as the key prop for the list of items. While this may seem convenient, it can lead to problems, mainly when the list is dynamic and items can be added, removed, or reordered.

Better approach

import React from ‘react’;

const ListItems = () => {

  const items = [

    { id: 1, name: ‘Apple’ },

    { id: 2, name: ‘Banana’ },

    { id: 3, name: ‘Orange’ }

  ];

  return (

    <ul>

      {items.map((item) => (

        <li key={item.id}>{item.name}</li>

      ))}

    </ul>

  );

};

export default ListItems;

To avoid using the index as the key, developers can use unique identifiers associated with each list item, such as database IDs or other unique attributes. Using unique identifiers ensures that each list item has a stable identity, even if the list order changes or items are added or removed

Modifying the Component state directly

Modifying the state directly in React is considered an anti-pattern. When you modify the state directly, React doesn’t detect the change, which can lead to unexpected behaviour in your application’s rendering. React relies on the setState function to properly trigger re-renders when state changes occur.

State updates in React are asynchronous, meaning React batches multiple state updates for performance reasons. When you modify the state directly, you risk introducing race conditions and inconsistent state updates, as React won’t be able to properly batch and synchronize the updates.

Example

import React, { useState } from “react”;

const Counter = () => {

  const [count, setCount] = useState(0);

  const increment = () => {

    // Modifying state directly (bad practice)

    count++; // Incorrect way to update state

    setCount(count); // Updating state using setState

  };

  return (

    <div>

      <p>Count: {count}</p>

      <button onClick={increment}>Increment</button>

    </div>

  );

};

export default Counter;

To avoid the mutable state anti-pattern, always use the setState function to update the state in React components. This ensures that React can properly track state changes and trigger re-renders as needed.

Not Using the useCallback Hook When It Would Be Beneficial

The React useCallback Hook returns a memoized callback function that helps to improve the performance of your code. Not using the useCallback hook when it can be done is indeed an anti-pattern in React.

Functions declared inside functional components are re-created on every render. When these functions are passed down to child components as props, it can lead to unnecessary re-renders of those child components. By using useCallback, you can memoize the function so that it’s only re-created when its dependencies change, reducing unnecessary re-renders.

Here is a simple example:

In the code, we have two parent components:

With useCallback()

const ParentWithCallback = () => {

    const [value, setValue] = useState(“”);

 

    const callback = useCallback(() => {

      console.log(“Operation performed with useCallback”);

    }, []);

 

    return (

      <div>

        <ChildComponent callback={callback} />

        <input

          value={value}

          placeholder=“Type here (with useCallback)”

          onChange={(e) => setValue(e.target.value)}

        />

      </div>

    );

  };

 

Without useCallback()

 

const ParentWithoutCallback = () => {

    const [value, setValue] = useState(“”);

 

    const callback = () => {

      console.log(“Operation performed without useCallback”);

    };

 

    return (

      <div>

        <ChildComponent callback={callback} />

        <input

          value={value}

          placeholder=“Type here (without useCallback)”

          onChange={(e) => setValue(e.target.value)}

        />

      </div>

    );

  };

The one without the useCallback hook has a simple callback function, which will rerender on each state change as a new reference to the function is created on each render. This can lead to unnecessary re-renders of ChildComponent, even when ParentWithoutCallback re-renders due to state changes.

The component with the useCallback hook is much better in terms of performance. The function reference remains stable across renders as long as the dependencies array (second argument of useCallback) remains unchanged. Since the callback has no dependencies in this example, it only gets created once and keeps the same reference across renders.

Spreading props directly onto DOM elements

Spreading props directly onto DOM elements can indeed lead to the addition of unknown HTML attributes

Let’s take an example:

const Sample = () => (

    <Spread flag={true} className=“content” />

  );

  const Spread = (props) => (

    <div {props}>Test</div>

  );

 The Spread component is spreading all props (flag and className) onto the <div> element without filtering them. And we know that <div> has no attribute called flag in its definition

If any unrecognised HTML attributes are included in the props passed to Spread, they will be added to the rendered DOM element. This can lead to unexpected behaviour or errors, as unknown HTML attributes may not be handled correctly by React or the browser.

The better approach is to create props specifically for the DOM attribute

const Sample = () => (

    <Spread flag={true} domProps={{className: “content”}} />

  );

  const Spread = (props) => (

    <div {props.domProps}>Test</div>

  );

In this approach, the domProps prop is specifically used for passing DOM attributes, such as className. By spreading only domProps onto the div element, we ensure that only recognized HTML attributes are included in the rendered output.

Props Plowing

Props plowing can occur when components have a large number of props that need to be passed down to child components. This can lead to repetitive and verbose code, making it harder to maintain and understand.

const data = {

    id: 11,

    name: Adarsh Gupta,

    age: 20,

    avatar: “👦🏻”,

    bio: “Student from Maths Department”

   }

   <StudentData id={data.id} name={data.name} age={data.age} avatar={data.avatar} bio={data.bio}/>

   <StudentData {data}/>

Using spread operators (…props) can help remove this issue by reducing the amount of repetitive code needed to pass props down to child components. 

Avoid Deep Nested Callbacks

Nested callbacks, where functions are nested within each other, can make code harder to understand and maintain. Instead of having deeply nested callback functions, it’s better to break down complex logic into smaller, more manageable functions

Bad approach

const fetchDataApi = () => {

    fetchData().then((data) => {

      process(data).then((result) => {

        updateState(result);

      });

    });

  };

In this code, fetching data from an API, processing it, and updating the state are all nested within callback functions, making it difficult to follow the flow of the code.

Better approach

const fetchDataAndProcess = async () => {

    const data = await fetchData();

    const result = await process(data);

    updateState(result);

  };

Here, we have broken down the logic into separate functions: one for fetching and processing the data asynchronously using async/await, and another for updating the state. This approach makes the code easier to understand and maintain.

Wrapping it up

Thank you for taking the time to read this blog. You are a fantastic reader. 

In this detailed blog we have looked into several anti-patterns in React that are important to steer clear of in your next React project. These anti-patterns can introduce various performance issues into your codebase, which can ultimately impact the user experience and maintainability of your application.

Throughout the blog, we have not only identified these anti-patterns but have also provided insights into why they should be avoided. Now you can start to write some high quality React applications.

Keep learning!

0
Adarsh M

Adarsh M

A soon-to-be engineer and software developer. Curious about advanced tools and Web technologies. Loves to be a part of fast moving technical world.

Add comment