DEV Community

Cover image for React: Essential Concepts and Best Practices
Bogdan Varlamov
Bogdan Varlamov

Posted on

React: Essential Concepts and Best Practices

Introduction

A overview to refresh your knowledge of React's core functionality.

Let's take a look at a React app and explore ways to improve the code.

Initial code

export default function App() {
  const cars = [
    {
      name: "Lightning McQueen ⚡️",
      image:
        "https://i.pinimg.com/1200x/c4/50/07/c45007f8ec1d37926d87befde23ec323.jpg",
    },
    // ...
  ];

  return (
    <div className="App">
      <Container cars={cars} />
    </div>
  );
}

function Container({ cars }) {
  let title = "Participants";
  const [isTitleUppercase, setTitleUppercase] = useState(false);

  const generateExtraParams = () => {
    return {
      position: 1,
      selfConfidence: Infinity,
    };
  };

  const handleClick = () => {
    setTitleUppercase((prev) => !prev);
  };

  return (
    <>
      <header>
        <h1>{isTitleUppercase ? title.toUpperCase() : title}</h1>{" "}
        <button onClick={handleClick}>Case toggle 🖲️</button>
      </header>
      <main>
        {cars.map((car, index) => (
          <CarCard
            key={index}
            car={car}
            // extraParams={generateExtraParams()}
          />
        ))}
      </main>
    </>
  );
}

function CarCard({ car, extraParams }) {
  console.log(`🏎️ Ka-Chow! ⚡️⚡️⚡️`);

  return (
    <>
      <p>{car.name}</p>
      <div id="iw">
        <img src={car.image} width={100} />
      </div>

      {extraParams &&
        Object.entries(extraParams).map(([key, value]) => (
          <p key={Math.random()}>
            <i>{key}:</i> {value}
          </p>
        ))}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

1. Prevent Unnecessary Re-renders

Now, when we click the 'Case toggle' button, we see that CarCard is re-rendering. Why does this happen and what can we do about it?

This happens because the button handler exists in the parent component, which triggers a re-render of that component and all its children. To fix this, we can use React.memo to wrap our CarCard component. React.memo is a higher-order component that will only re-render the wrapped component if its props change. This way, CarCard won't re-render unless its specific props change.

Here's how we can implement it:

const CarCard = React.memo(function ({ car, extraParams }) {
  console.log(`🏎️ Ka-Chow! ⚡️⚡️⚡️`);

  return (
    <>
      <p>{car.name}</p>
      <div id="iw">
        <img src={car.image} width={100} />
      </div>

      {extraParams &&
        Object.entries(extraParams).map(([key, value]) => (
          <p key={Math.random()}>
            <i>{key}:</i> {value}
          </p>
        ))}
    </>
  );
});

Enter fullscreen mode Exit fullscreen mode

2. Memoize Function Calls

When you uncomment the line extraParams={generateExtraParams()} and click the 'Case toggle' button, you'll notice that CarCard re-renders. This happens because generateExtraParams creates a new object on every render, and React treats it as new data.

To prevent this, we use React.useMemo to memoize the result of generateExtraParams. This ensures that the function is only called once and the same object is used on subsequent renders, preventing unnecessary re-renders.

const generateExtraParams = () => {
  return {
    position: 1,
    selfConfidence: Infinity,
  };
};

const extraParams = React.useMemo(() => generateExtraParams(), []);

Enter fullscreen mode Exit fullscreen mode

3. Create a Custom Hook

Can we create a custom hook to manage the toggle uppercase functionality, since we might need this in multiple places in the future? Absolutely. Let's implement it:

const useToggle = () => {
  const [isTitleUppercase, setTitleUppercase] = useState(false);

  const toggle = () => {
    setTitleUppercase((prev) => !prev);
  };

  return [isTitleUppercase, toggle];
};

// Inside the Container
const [isTitleUppercase, toggle] = useToggle();

// And use it like this:
<header>
  <h1>{isTitleUppercase ? title.toUpperCase() : title}</h1>{" "}
  <button onClick={toggle}>Case toggle 🖲️</button>
</header>
Enter fullscreen mode Exit fullscreen mode

We moved the state and function to a custom hook, which we can reuse in different places as needed.

4. Use Context for Deep Prop Drilling

Now, if we need to pass a prop deeply through the component tree, can we do it better than just prop drilling? Yes, we can use context for this.

React Context is a way to manage state globally. It allows you to create a context object, which can then be provided to the component tree. Any component in the tree can consume the context value without needing to pass props through every level.

Here's how we can implement it:

  1. Create Context:
    We create a context object using React.createContext() and provide a default value. In this case, we'll create a context for our cars.

  2. Provide Context:
    We use a context provider to wrap our component tree. This makes the context value available to all components within the provider.

  3. Consume Context:
    Any component that needs the context value can use the useContext hook to access it.

We also create a custom hook useCarsContext to make it easier to consume the context. This hook encapsulates the useContext call, so we don't need to repeat it in every component that needs the context value.

Here's how the updated code looks:

import React, { useMemo, useState, useContext, createContext } from "react";

const CarsContext = createContext([]);
const useCarsContext = () => useContext(CarsContext);

export default function App() {
  const cars = [
    {
      name: "Lightning McQueen ⚡️",
      image:
        "https://i.pinimg.com/1200x/c4/50/07/c45007f8ec1d37926d87befde23ec323.jpg",
    },
    // ...
  ];

  return (
    <CarsContext.Provider value={cars}>
      <div className="App">
        <Container />
      </div>
    </CarsContext.Provider>
  );
}

const useToggle = () => {
  const [isTitleUppercase, setTitleUppercase] = useState(false);

  const toggle = () => {
    setTitleUppercase((prev) => !prev);
  };

  return [isTitleUppercase, toggle];
};

function Container() {
  let title = "Participants";
  const [isTitleUppercase, toggle] = useToggle();

  const generateExtraParams = () => {
    return {
      position: 1,
      selfConfidence: Infinity,
    };
  };

  const extraParams = useMemo(() => generateExtraParams(), []);
  const cars = useCarsContext();

  return (
    <>
      <header>
        <h1>{isTitleUppercase ? title.toUpperCase() : title}</h1>{" "}
        <button onClick={toggle}>Case toggle 🖲️</button>
      </header>
      <main>
        {cars.map((car, index) => (
          <CarCard key={index} car={car} extraParams={extraParams} />
        ))}
      </main>
    </>
  );
}

const CarCard = React.memo(function ({ car, extraParams }) {
  console.log(`🏎️ Ka-Chow! ⚡️⚡️⚡️`);

  return (
    <>
      <p>{car.name}</p>
      <div id="iw">
        <img src={car.image} width={100} />
      </div>

      {extraParams &&
        Object.entries(extraParams).map(([key, value]) => (
          <p key={Math.random()}>
            <i>{key}:</i> {value}
          </p>
        ))}
    </>
  );
});
Enter fullscreen mode Exit fullscreen mode

5. Avoid Using Index and Math.random() for Keys

One last improvement: using index and Math.random() for keys in lists is not a good practice. Instead, use a unique identifier related to the item. This ensures React can track which items are added, moved, or removed correctly.

Conclusion

I hope this refreshed your memory about core React functionality. By exploring these basic examples and improvements, we've seen how to make our React apps more efficient and easier to maintain.

Thanks for hanging out! Hit subscribe for more! 👋

Top comments (0)