<ggalupo />

Avoiding redundant states in React

Learn how to identify and prevent unnecessary states

4m read

When we want to display on screen some information that will be modified as the user interacts with our application, it is common to think about storing it in a state variable, which will be updated according to this interaction.

However, if this information depends on an existing state, some precautions can be taken to prevent bugs and performance issues.

Common mistakes when displaying information derived from an existing state

To exemplify, let's imagine a shopping cart component used to sale event tickets, where whenever the user changes the desired amount of tickets, the total purchase price should also be updated.

❌ Create a new state and update it whenever the original state is modified

In this scenario, inconsistencies in the information may occur, since it will be necessary to manually update the value of one state whenever the other one changes.

Cart.tsx
1"use client";2import { useState } from "react";34import { currencyFormatter } from "@/utils/currency";56const TICKET_PRICE = 15.0;78export const Cart = () => {9  const [ticketAmount, setTicketAmount] = useState(0);10  const [totalPrice, setTotalPrice] = useState(0);1112  const handleTicketIncrement = () => {13    const newTicketAmount = ticketAmount + 1;14    setTicketAmount(newTicketAmount);15    setTotalPrice(newTicketAmount * TICKET_PRICE);16  };1718  const handleTicketDecrement = () => {19    const newTicketAmount = ticketAmount - 1;20    setTicketAmount(newTicketAmount);21    // Haven't updated totalPrice state22  };2324  return (25    <>26      <button onClick={handleTicketDecrement}>-</button>27      <span>Tickets: {ticketAmount}</span>28      <button onClick={handleTicketIncrement}>+</button>29      <span>Purchase price: {currencyFormatter(totalPrice)}</span>30    </>31  );32};33

In the code snippet above, on line 21, the totalPrice state was not updated in the handleTicketDecrement function, which handles decrementing the tickets counter. Thus, its value will get outdated when the function is invoked, causing incorrect information to be displayed on the screen.

❌ Create a new state and update it as a side effect of updating the original state

With the implementation below, every time the value of the ticketAmount state changes, the totalPrice state will automatically update.

Cart.tsx
1"use client";2import { useEffect, useState } from "react";34import { currencyFormatter } from "@/utils/currency";56const TICKET_PRICE = 15.0;78export const Cart = () => {9  const [ticketAmount, setTicketAmount] = useState(0);10  const [totalPrice, setTotalPrice] = useState(0);1112  const handleTicketIncrement = () => {13    setTicketAmount(ta => ta + 1);14  };1516  const handleTicketDecrement = () => {17    setTicketAmount(ta => ta - 1);18  };1920  useEffect(() => {21    setTotalPrice(ticketAmount * TICKET_PRICE);22  }, [ticketAmount]);2324  return (25    <>26      <button onClick={handleTicketDecrement}>-</button>27      <span>Tickets: {ticketAmount}</span>28      <button onClick={handleTicketIncrement}>+</button>29      <span>Purchase price: {currencyFormatter(totalPrice)}</span>30    </>31  );32};33

Although totalPrice has the correct value now, each new update to the ticketAmount state will result on an unnecessary re-render of the component.

This happens because useEffect will cause totalPrice to be updated only in a new rendering flow, which can lead to performance issues in applications with a large amount of state updates.

setTicketAmount(ta => ta + 1) is a way to update the state based on its previous value

The best way to display information derived from an existing state

✅ Calculate it during component rendering

Ideally, the information should be calculated while the component is rendering. This ensures that it will be always up-to-date, as well as avoiding unnecessary re-renders, which optimizes application performance and helps bug prevention.

Cart.tsx
1"use client";2import { useState } from "react";34import { currencyFormatter } from "./utils/currency";56const TICKET_PRICE = 15.0;78export const Cart = () => {9  const [ticketAmount, setTicketAmount] = useState(0);10  const purchasePrice = ticketAmount * TICKET_PRICE;1112  const handleTicketIncrement = () => {13    setTicketAmount(ta => ta + 1);14  };1516  const handleTicketDecrement = () => {17    setTicketAmount(ta => ta - 1);18  };1920  return (21    <>22      <button onClick={handleTicketDecrement}>-</button>23      <span>Tickets: {ticketAmount}</span>24      <button onClick={handleTicketIncrement}>+</button>25      <span>Purchase price: {currencyFormatter(purchasePrice)}</span>26    </>27  );28};29

In the example above, purchasePrice is calculated during component rendering, based on the current value of ticketAmount. Thus, whenever ticketAmount is updated, purchasePrice will also update accordingly, without the need for additional state updates or unnecessary re-renders.

Conclusion

Summarizing, when dealing with data derived from an existing state in React, it's important to avoid creating new states whenever possible. Instead, aim to compute them directly during component rendering, thereby improving the quality of your code.