Back

Common React Mistakes Front End Developers Make

Common React Mistakes Front End Developers Make

React JS is a powerful and popular library for building user interfaces but has some pitfalls. In this article, we’ll explore common mistakes front-end developers often make when working with React and how to avoid them. React Js gives us warnings (messages that help us understand the error we encounter while coding) - when we navigate to the console part of our web browsers. However, even with these warnings, it is still hard to proffer solutions to some of these errors, so this article will help you.

This article explores some common mistakes and solutions in React JS; without further ado, let’s jump right in.

Mutating States Directly

States in React Js are immutable, and developers shouldn’t make the mistake of trying to update them directly. This mistake often surfaces when developers are working with a state that is a non-primitive data type, such as arrays and objects. For instance, when a developer works with a state of the datatype - array, the developer tends to make the mistake of updating the state using the push method. That is wrong, and React won’t be able to detect that the developer updated the state unless we use the setter function that comes with the state.

The tricky part is that mutating states directly works in some scenarios and fails in others. Either way, we must know that it is a wrong approach to mutate the state directly. The right strategy for updating a state is to use the setter function.

Suppose we have a simple program with an input field in which we input a programming language and then add the language to the list of existing languages.

Here is the code sample where the mutating state directly works:

import React, { useState } from "react";

export function App() {
  const [languages, setLanguages] = useState(["JavaScript"]);
  const [newLanguage, setNewLanguage] = useState("");

  function addNewLanguage(language) {
    if (language.trim() !== "") {
      languages.push(language);
      setLanguages(languages);
      setNewLanguage("");
    }
  }

  return (
    <div className="App">
      <h1> Various Programming Languages</h1>
      <input
        type="text"
        placeholder="Enter a programming language"
        value={newLanguage}
        onChange={(e) => setNewLanguage(e.target.value)}
      />
      <button onClick={() => addNewLanguage(newLanguage)}>Add Language</button>
      <ul>
        {languages.map((language, index) => (
          <li key={crypto.randomUUID()}>{language}</li>
        ))}
      </ul>
    </div>
  );
}

In line 7 of the sample code above, we mutated the state directly using the array push method. When you run the code – add a new programming language using the input field, and then click the button to add the new programming language. You will agree that the newly added programming language is added to the state array of the programming languages and then displayed.

The question that comes to mind is, “Why does this approach work?” instead of React’s mechanism of mutating state not being allowed! What is happening is that we are mutating the array state in place and implementing it within the same component. But when we send that array state as props to another component, the application no longer works, which is the expected result.

Here is the code where we mutate the state and share it as props across various components:

When you run the code by adding a new language, React doesn’t add the newly added language to the state, nor is it displayed. If you then navigate to the problems tab in the code playground above, you will notice an error that says “Impossible State” - this signifies the impossibility of mutating the state.

The cause of the problem

React uses a mechanism called Reconciliation to update the virtual DOM. Reconciliation is a process whereby React compares the previous and new states to detect if there is any need to re-render the components. In the first code above, where we dealt with a single component and mutating state using the push method, it worked because we weren’t sharing the state as props to another component. In the code above, when we mutate the state, React notices that the array’s identity hasn’t changed, so it assumes nothing was added or removed; therefore, it doesn’t update the props.

The Solution

Now that we have detected the error and the reason, let’s see how to fix it. To fix this code to work correctly, we need to implement a syntax called the Spread operator (...).

Navigate to the “App.jsx” file and update the addNewLanguage() to this:

function addNewLanguage(language) {
  if (language.trim() !== "") {
    const allLanguages = [...languages, language];
    setLanguages(allLanguages);
  }
}

In the code snippet above, we created an array named allLanguages. Then, we used the spread operator to copy all the items currently in the state array named Languages into this new array. We then added the new programming language to the new array and updated the state using the setter function. In simple terms, we created a new array identity and then passed it to the state. React detects a change and can update the shared props to other components.

Missing Keys in Mapped List

This mistake is two in one. Here, we would examine the mistakes and proceed to the solution.

First mistake

React developers (beginners) forget to add and assign a value to the key prop when mapping an iterable element. One common occurrence is when we have an array and want to display the items in a list-like view. If we fail to add the key prop to each child in the list, you get an error that says, “Warning: Each child in a list should have a unique key prop.”

How do we get a value for the key prop? This question leads to the second mistake.

Second mistake

In getting a value for the key prop, some developers jump to the most straightforward solution: the index value obtained from the map method. You shouldn’t use the index from the map method as the key prop value. Here is why:

  • Instability of Indexes: Indexes are not stable. For instance, if you have a list of items and, for some reason, the order changes or we randomly delete two of the items, this will also lead to a change in the indexes of the items. When React applies the diffing algorithm to see if the component has new changes, we encounter a problem updating the DOM.
  • Indexes aren’t unique: To refresh our memory, the error for this section is “Warning: Each child in a list should have a unique key prop.” The latter part of this error explicitly says that we need a unique value for the key prop. Indexes are not unique; the same index can be assigned to two identical (duplicate) list items.
  • Indexes are less efficient in terms of memory and time. The larger the list of items, the more time and space we need to generate the indexes.

The Solution

First, we must remember that the value must be unique. An excellent and optimal solution is using this: crypto.randomUUID().

Why crypto.randomUUID()?

  • Uniqueness: The crypto.randomUUID() function generates a universally unique identifier (UUID). This ensures that each key is unique, addressing the requirement for a unique identifier in the warning.
  • Stability: Unlike indexes, UUIDs remain stable regardless of the order or changes in the list. This stability is crucial for React’s reconciliation process, allowing efficient updates to the DOM.
  • Universality: UUIDs are designed to be universally unique, reducing the risk of collisions. This universality ensures that the probability of generating duplicate keys is extremely low, even in scenarios with many items.
  • Consistency: Using crypto.randomUUID() provides a consistent approach to generating keys, making the codebase more maintainable and reducing the likelihood of errors related to key generation.

This method generates unique id like 550e8400-e29b-41d4-a716-446655440000.

Henceforth, you should use crypto.randomUUID() as the key prop for your mapped items. Here is a simple implementation of crypto.randomUUID():

function ProgrammingLanguages({ languages }) {
  return (
    <ul>
      {languages.map((language) => (
        <li key={crypto.randomUUID()}>{language}</li>
      ))}
    </ul>
  );
}

Using Multiple useState Hooks for the same Entity

As a front-end developer, you are familiar with the authentication UI codebase. This UI codebase is what I will make use of to explain the mistake in this section.

Here is a code snippet to visualize the mistake:

import React, { useState } from "react";
import "./styles.css";

export default function App() {
  const [email, setEmail] = useState("");
  const [name, setName] = useState("");
  const [password, setPassword] = useState("");
  const [confirmPassword, setConfirmPassword] = useState("");

  return (
    <div className="container">
      <form>
        <h2>Register</h2>
        <div className="form-group">
          <input
            type="text"
            value={name}
            onChange={(e) => setName(e.target.value)}
            placeholder="Janes James"
            required
          />
        </div>

        <div className="form-group">
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            placeholder="email@gmail.com"
            required
          />
        </div>

        <div className="form-group">
          <input
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            placeholder="Password"
            required
          />
        </div>

        <div className="form-group">
          <input
            type="password"
            value={confirmPassword}
            onChange={(e) => setConfirmPassword(e.target.value)}
            placeholder="ConfIrm Password"
            required
          />
        </div>

        <button type="submit">Register</button>
      </form>
    </div>
  );
}

From the code snippet above, you can spot the mistake, which is the use of the useState hook multiple times for the same entity (a signup/registration form).

Let’s look at how to update the codebase above and make it more presentable and professional without wasting time.

In the code above, we updated three different parts:

  • First, we revamped the state instances by embedding all of them in just a single state object.
  • Next, we created a function named handleChange to handle the onChange attributes for the input fields. We destructured each input’s name and value property in the newly created function. Then, we updated the state object using the spread operator, calling the destructured name property and assigning it to the value property.
  • Lastly, we passed the handleChange function as a value for the onChange attributes. We also added the name property to each input field.

In addition, you can also make use of the useReducer hooks to manage this type of instance.

Poor File Structure

File structuring is akin to the blueprint & foundation of a building structure. A well-structured project directory gives room for code maintainability and usability, scalability/code upgrade, and it also promotes collaboration amongst developers. It is no news that proper file/directory structuring is an underestimated/neglected aspect of development, which, if not given attention, would later affect the overall development flow.

One important thing we must learn is to break down and detect the suitable location of a file in the project directory based on its contents or logic.

Here are some folder samples in a standard React Project Directory:

  • /src: This default folder comes with a newly bootstrapped React project. It is the root directory of the source code.
  • /components: You create this by yourself. This directory contains all the reusable React components for your project.
  • /pages: This directory stores the code for an individual page or view of a part of the application.
  • /utils: This directory contains utility or helper functions we share across various application parts.
  • /assets: In this directory, we embed images. Some developers also choose to embed their custom CSS files in this directory. In such a situation, this directory will have two sub-directories: /images and /styles.
  • /constants: Here, we store files that contain constant values that we believe won’t change during the execution of the application.

Below are two file structure hierarchies - one representing a poor/inefficient structure and the other a correct and proper structure.

Poor/Inefficient File Structure

/src
  ├── MainApp.js
  ├── Navigation.js
  ├── FooterComponent.js
  ├── FeatureComponent1.js
  ├── FeatureComponent2.js
  ├── FeatureComponent3.js
  ├── helper.js
  ├── styling.css
  ├── images/
      ├── mainImage.jpg
      ├── icon.png
  ├── constants.js
  ├── HomePage.js
  ├── AboutUsPage.js
  ├── ContactPage.js

The file structure above is considered poor due to its flat organization, where all files, including components, styles, helpers, images, constants, and pages, are placed in the root directory (/src). This lack of hierarchical structure makes it challenging to maintain and understand the codebase as it expands. Additionally, the absence of clear component grouping, such as a dedicated directory for pages, components, or styles, contributes to a lack of organization. The global placement of the styling file (styling.css) and helper functions (helper.js) in the root directory, along with the global constants file (constants.js), further hinders modularity and makes it difficult to identify dependencies. Inconsistencies in naming conventions among components add to the codebase’s complexity.

Correct and Proper File Structure

/src
  ├── components/
      ├── Header.js
      ├── Footer.js
      ├── FeatureCard.js
      ├── FeatureCarousel.js
  ├── pages/
      ├── Home.js
      ├── About.js
      ├── Contact.js
  ├── utils/
      ├── apiHelper.js
  ├── constants/
      ├── constants.js
  ├── assets/
      ├── images/
          ├── mainBanner.jpg
          ├── featureIcon.png
      ├── styles/
          ├── mainStyles.css
  ├── App.js

The file structures above use real-world examples to demonstrate how thoughtful naming and organization can enhance the overall quality of a React JS project. Here, we separated our file structure into sub-folders under the root folder (/src). The name of each sub-directory is based on the functionality. For instance, /components takes in everything that has to do with the components.

Not Utilizing React Fragments to Wrap Adjacent JSX Elements

Just as JavaScript doesn’t allow us to return multiple elements in a function, so does React.

In JavaScript, if you have a function like the one below, you will get a syntax error.

function displayHelloWorld() {
  // This will result in a syntax error
  return (
    <div>Hello</div>
    <p>World</p>
  );
}

Back to React JS;

When rendering multiple elements, you must wrap them around an element tag representing a parent element. In interpreting the React JS code, React checks if we embed everything we need to render in a single parent element. If it isn’t, it throws an error; otherwise, it proceeds to check through the code in the rendering environment.

Example of a code with the mistake:

import React from 'react';

const PersonalProfile = ({ user }) => {
  return (
      <h2>{user.name}</h2>
      <p>{user.mail}</p>
  );
};

export default PersonalProfile;

The code above returns an error: “Adjacent JSX elements must be wrapped in an enclosing tag.”

Initially, developers tilted to using a div element to resolve the error.

import React from "react";

const PersonalProfile = ({ user }) => {
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.mail}</p>
    </div>
  );
};

export default PersonalProfile;

The code above is the method initially adopted by the developers, but it posed another issue. The issue is that we are adding an unnecessary markup to the Document Object Model (DOM). Due to this, React created a better and optimal solution that doesn’t affect the DOM.

React introduced an element called React Fragment. This element helps the developers wrap multiple elements together.

import React from "react";

const PersonalProfile = ({ user }) => {
  return (
    <React.Fragment>
      <h2>{user.name}</h2>
      <p>{user.bio}</p>
    </React.Fragment>
  );
};

export default PersonalProfile;

Now, there is a shorter way to implement the React Fragment.

import React from "react";

const PersonalProfile = ({ user }) => {
  return (
    <>
      <h2>{user.name}</h2>
      <p>{user.mail}</p>
    </>
  );
};

export default PersonalProfile;

The introduction of <>....</> for React Fragments fully confirms that, indeed, <React.Fragment>...</React.Fragment> isn’t adding any markup to the DOM.

Neglecting Optional Chaining for API Data

Optional chaining is essential when rendering data. In most cases, the response obtained from querying an API is usually nested objects. When fetching data and per adventure, you try to access an undefined or null property. This action will break up the app and throw an error: “Cannot read property of undefined.”

Let’s assume that there is an API called ProfileAPI. When you request data from it, the response is in this simple format:

{
  user: {
    bioData: {
      primary: {
        name: "Dev";
      }
      secondary: {
        age: 20;
      }
    }
  }
}

From the JSON response format above, everything works well if you try to retrieve any present data: data.user.biodata.primary.name or data.user.biodata.secondary.age. But you get an error once you try accessing data that is not present: data.user.biodata.primary.mail without the optional chaining (?).

Don’t fetch data like this:

const userName = data.user.biodata.primary.name;

Henceforth, do this:

const userName = data?.user?.biodata?.primary?.name;

Note: Optional Chaining short-circuits the evaluation of any property in the chain (nested objects) is not present.

Conditionally Rendering Items without Consideration of Falsy and Truthy Values

In JavaScript, we refer to some values as falsy and some as truthy. Like the name falsy, these values are evaluated as false in the context of the usage of boolean, and for truthy, they get evaluated as true.

Examples of falsy values include undefined, null, 0, NaN, an empty string (” ”), and, of course, false. Values outside these falsy values are truthy values, any expression or statement that evaluates to true.

Boolean Operators

  • || - Logical AND
  • && - Logical OR

These boolean operators are used for short-circuiting. Short-circuiting is a technique that takes advantage of the fact that the result of a logical expression can be determined without evaluating all of its parts.

  • Short-Circuiting with && (Logical AND): The && operator returns the first falsy operand or the last operand if all are truthy. If the first operand is falsy, the second operand is not evaluated because the entire expression is already known to be falsy.

Implementation:

// Using && for short-circuiting
let result = (true && "Hello, World!"); // result is "Hello, World!"

// If the first operand is falsy, the second operand is not evaluated
result = (false && "This won't be evaluated!"); // result is false
  • Short-Circuiting with || (Logical OR): The || operator returns the first truthy operand or the last operand if all are falsy. If the first operand is truthy, the second operand is not evaluated because the entire expression is already known to be truthy.

Implementation:

// Using && for short-circuiting
let result = (true && "Hello, World!"); // result is "Hello, World!"

// If the first operand is falsy, the second operand is not evaluated
result = (false && "This won't be evaluated!"); // result is false

The issue

Developers commonly make mistakes when rendering elements conditionally because they don’t consider falsy values.

Let’s take a look at this sample code:

With the code above, we aim to be able to render the Solution component provided that the length of the array mistakes is equal to or greater than one; otherwise, we won’t render anything. Surprisingly, the code above doesn’t give us that but renders 0. Since the expression before the && operator evaluates to 0, which is a falsy value. JavaScript automatically short-circuits the expression, resulting in 0. And since 0 is a valid value in JSX, it gets rendered.

To solve this, we can alter the code by adequately checking if the array’s length isn’t zero and returning a boolean value - true or false.

Now, if there isn’t any item in the array, the array evaluates to false; since false isn’t a valid JSX value, nothing gets rendered.

Alternatively, we can scale the code by using the ternary operator. Instead of leaving the User Interface (UI) blank when the array is empty, we can harness the power of the ternary operator by adding another expression that will be displayed when the array is empty.

In the code above, another expression is added to handle the scenario whereby the array is empty. So, instead of leaving it blank, we display something to the user.

Ignoring Dependency arrays in React hooks that require them

In React Js, some hooks require you to add a dependency array. The dependency array is more like a determining factor on how and when the hook initiates after the initial render. Missing the dependency array in instances where they are crucial can break the code or make it less efficient.

Issue

One frequent issue some developers encounter when working with hooks that utilize a dependency array is determining the value that goes into the array.

Solution

Let’s consider a popular scenario where we are fetching data from an external API, performing the action inside the useEffect hook. Here, we want to use the useEffect hook to make the API call when the component mounts and update the state.

The code format:

import React, { useState, useEffect } from "react";

const sampleFormat = ({ query }) => {
  const [data, setData] = useState([]);

  useEffect(() => {
    // Fetch data from an API
    fetchData(query)
      .then((result) => {
        // Update the state with the fetched data
        setData(result);
      })
      .catch((error) => {
        console.error("Error fetching data:", error);
      });
  }, []); // What should go into the dependency array?

  const fetchData = async () => {
    // Simulating an API call
  };

  return <div>{/* Render the component using the 'data' state */}</div>;
};

export default sampleFormat;

The useEffect dependency array is still empty. Now, let’s learn how we can determine what value should be in it.

Step 1: Ask yourself what the dependencies are. Take dependencies as the things your action (fetching data from an API) depends on.

In our code, you will agree that the fetchData() function is a dependency since it is a significant stakeholder in helping us achieve our action inside the hook.

Step 2: The next question comes to mind: “Are there any other values that should be in the dependency array?”

Since we have been able to detect one value, we would make use of that. Going into the function, we find that it depends on another external prop value, which it takes as an argument. This prop is also a valid value for the dependency array.

Now that we have detected the values, let’s add them.

import React, { useState, useEffect } from "react";

const sampleFormat = ({ query }) => {
  const [data, setData] = useState([]);

  useEffect(() => {
    // Fetch data from an API
    fetchData(query)
      .then((result) => {
        // Update the state with the fetched data
        setData(result);
      })
      .catch((error) => {
        console.error("Error fetching data:", error);
      });
  }, [fetchData, query]); // What should go into the dependency array?

  const fetchData = async (param) => {
    // Simulating an API call
  };

  return <div>{/* Render the component using the 'data' state */}</div>;
};

export default sampleFormat;

Tips

  • Always add functions used in the hook as a dependency array.
  • Add external variables, states, or props that the action depends on, just like in our code.
  • Turn on the esLint extension in your Visual Studio Code. It suggests the values that should be in your dependency array.

Prop Drilling

Prop drilling is a common mistake React developers make when dealing with state management in their applications. It occurs when a parent component generates its state and passes it down as props to children components that do not consume the props but only pass it down to another component that finally consumes it. This practice can lead to several issues, including reduced code maintainability, decreased readability, and increased complexity.

Examining a simple codebase:

// GrandparentComponent.js
const GrandparentComponent = () => {
  const data = "Prop Drilling";

  return <ParentComponent data={data} />;
};

// ParentComponent.js
const ParentComponent = ({ data }) => {
  return <MotherComponent data={data} />;
};

// ChildComponent.js
const ChildComponent = ({ data }) => {
  return <div>{data}</div>;
};

In the code above, the ChildComponent must access a prop value from the GrandParentComponent. To get this prop value, we pass it via another component that does not need it. This approach works, but we are concerned about more than just the code working but how efficient the code is. In an efficient code, data we don’t need in a component shouldn’t be in that component. How, then, can we achieve this? We need to use a proper state management tool. React provides us with an internal one called React Context API. Some external ones include Redux Toolkit, Zustand, and so on.

React Context API

With React Context API, we can pass data from any component directly to another component without passing the data to a component that won’t consume it. Still using the simple codebase above, let’s see how to implement the React Context API.

Here is the code implementation:

In the code above, we first created a new context using the createContext function provided by React. This context will be used to share data across components. Then we used the useContext hook to access the current value of the context. If the context created is undefined, it means that the useData hook is not being used within a DataProvider. In a case like this, it throws an error indicating that the useData hook must be used within a DataProvider. If the context is available, it is returned, allowing components to access the data provided by the DataProvider.

Inside the GrandParentComponent, we populated the data and passed it to the DataProvider. And lastly, ChildComponent consumes the data using a custom context hook - useData.

Not Understanding the Component LifeCycle

Just because we are no longer in the era of Class Components, some React developers no longer see the need to learn about the Component LifeCycle. Meanwhile, every React Developer must have a good understanding of what this LifeCycle entails.

A Component’s LifeCycle consists of three main phases: the Mounting Phase, the Updating Phase, and the Unmounting Phase.

Here is the mode of entry for each phase:

  • Mounting Phase:

    • constructor(): Start: Initiated when we create a component. End: The constructor gets completed once the initial setup is done.
    • static getDerivedStateFromProps(): Start: Initiated before every render. End: It completes after updating the state based on the props, if necessary.
    • render(): Start: Called after getDerivedStateFromProps. End: It completes after returning the JSX representing the component’s structure.
    • componentDidMount(): Start: Invoked immediately after a component gets inserted into the DOM. End: It completes after performing any necessary side effects, such as data fetching, and the component is fully mounted.
  • The Updating Phase:

    • static getDerivedStateFromProps(): Start: Similar to the mounting phase, we call this before every render. End: It completes after updating the state based on new props.
    • shouldComponentUpdate(): Start: Called before rendering when new props or states are received. End: It completes after determining whether the component should re-render.
    • render(): Start: Called to compute the updated JSX whenever a component’s state or props change. End: It completes after returning the updated JSX.
    • getSnapshotBeforeUpdate(): Start: Called right before the most recently rendered output is committed to the DOM. End: It completes after capturing information from the DOM, like the scroll position.
    • componentDidUpdate(): Start: Invoked after the component updates and the changes reflected in the DOM. End: It completes after performing any side effects or additional data fetching.
  • Unmounting Phase:

    • componentWillUnmount(): Start: We call this method before a component gets removed from the DOM. End: It completes after cleanup tasks, such as canceling network requests or clearing timers.

Note: this section is just to give you an introduction to what Component lifecycle is about. Check the link below for an in-depth guide on what the Component lifecycle entails.

Here is a guide to learn more about component lifecycle.

Conclusion

In this article, we looked at a few common mistakes React JS developers tend to make. We also covered their respective solutions, which will help you write better, more scalable, and optimized code.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay