Back to Blog
When Your React State Is Lying to You: Derived State Done Right
8 min readSep 26, 20265 views

When Your React State Is Lying to You: Derived State Done Right

It's easy to fall into the trap of syncing a computed value into `useState`, leading to stale UI, extra renders, and subtle bugs. Let's fix derived state.

ReactFrontendSoftware DesignPerformanceState Management
Share

by Sunil Band

The Silent Killer: useState for Derived Values

We've all been there: you have some piece of data, let's call it items, and you need to compute something based on it, like selectedItemsCount. Your first instinct might be to reach for useState to store that selectedItemsCount, updating it whenever items changes. It feels natural, a simple way to cache a computation. But what if I told you this common pattern is often a silent bug waiting to happen, or at least a performance drain?

The problem isn't immediately obvious. Your component renders, items changes, you update selectedItemsCount in a useEffect. Everything seems fine. But you're introducing a new source of truth that needs explicit synchronization, leading to potential desynchronization, unnecessary re-renders, and making your component harder to reason about. You're fighting the very nature of React's render cycle.

Understanding Derived State

Derived state is any piece of state that can be computed entirely from other existing state or props. It's not a standalone, independent value. Think of it like a spreadsheet formula: the sum of a column is derived from the individual cells; it doesn't need its own separate SUM cell that you manually update every time a number changes.

In React, the golden rule for derived state is simple: compute it directly during render. React's render cycle is precisely for this kind of work. When your component re-renders, any derived values will automatically be re-computed based on the latest props and state. This ensures consistency and avoids the whole synchronization dance.

Let's look at a common scenario to illustrate this.

The Problematic useEffect Approach

Imagine you have a list of products, and you want to display the total price of all selected products. A common, yet flawed, approach might look like this:

typescript
import React, { useState, useEffect } from 'react';

interface Product {
  id: string;
  name: string;
  price: number;
  selected: boolean;
}

interface ProductListProps {
  initialProducts: Product[];
}

function ProductList({ initialProducts }: ProductListProps) {
  const [products, setProducts] = useState<Product[]>(initialProducts);
  const [totalSelectedPrice, setTotalSelectedPrice] = useState<number>(0); // \u274C Problematic derived state

  // \u274C This useEffect is a red flag for derived state
  useEffect(() => {
    console.log('Recalculating total selected price via useEffect');
    const newTotal = products.reduce((sum, p) => (p.selected ? sum + p.price : sum), 0);
    setTotalSelectedPrice(newTotal);
  }, [products]); // Dependency on products array

  const toggleProductSelection = (id: string) => {
    setProducts(prevProducts =>
      prevProducts.map(p => (p.id === id ? { ...p, selected: !p.selected } : p))
    );
  };

  return (
    <div>
      <h2>Products</h2>
      <ul>
        {products.map(product => (
          <li key={product.id}>
            <input
              type="checkbox"
              checked={product.selected}
              onChange={() => toggleProductSelection(product.id)}
            />
            {product.name} - ${product.price.toFixed(2)}
          </li>
        ))}
      </ul>
      <h3>Total Selected Price: ${totalSelectedPrice.toFixed(2)}</h3> {/* Displaying the derived state */}
    </div>
  );
}

// Usage example:
// <ProductList initialProducts={[
//   { id: '1', name: 'Laptop', price: 1200, selected: false },
//   { id: '2', name: 'Mouse', price: 25, selected: false },
//   { id: '3', name: 'Keyboard', price: 75, selected: false },
// ]} />

Why is this useEffect problematic? Every time products changes, the effect runs, potentially causing an extra re-render just to update totalSelectedPrice. If products is updated frequently, you're doing double the work. More critically, if there's ever a bug that prevents useEffect from running or if products somehow gets out of sync with what the useEffect expects it to be (e.g., due to a memoization issue or an external update that doesn't trigger the dependency array correctly), your totalSelectedPrice will display stale data. It's a fragile pattern.

The Correct Approach: Compute During Render

The fix is almost too simple: calculate totalSelectedPrice directly inside the render function. This ensures it's always up-to-date with the products state without any explicit synchronization logic.

typescript
import React, { useState } from 'react';

interface Product {
  id: string;
  name: string;
  price: number;
  selected: boolean;
}

interface ProductListProps {
  initialProducts: Product[];
}

function ProductList({ initialProducts }: ProductListProps) {
  const [products, setProducts] = useState<Product[]>(initialProducts);

  // \u2705 Derived state computed directly during render
  const totalSelectedPrice = products.reduce((sum, p) => (p.selected ? sum + p.price : sum), 0);

  const toggleProductSelection = (id: string) => {
    setProducts(prevProducts =>
      prevProducts.map(p => (p.id === id ? { ...p, selected: !p.selected } : p))
    );
  };

  return (
    <div>
      <h2>Products</h2>
      <ul>
        {products.map(product => (
          <li key={product.id}>
            <input
              type="checkbox"
              checked={product.selected}
              onChange={() => toggleProductSelection(product.id)}
            />
            {product.name} - ${product.price.toFixed(2)}
          </li>
        ))}
      </ul>
      <h3>Total Selected Price: ${totalSelectedPrice.toFixed(2)}</h3>
    </div>
  );
}

// Usage example (same as before):
// <ProductList initialProducts={[
//   { id: '1', name: 'Laptop', price: 1200, selected: false },
//   { id: '2', name: 'Mouse', price: 25, selected: false },
//   { id: '3', name: 'Keyboard', price: 75, selected: false },
// ]} />

Notice how much cleaner this is. No useEffect, no setTotalSelectedPrice, no extra state variable. The totalSelectedPrice is always a true reflection of the products state at the time of render. This is robust, efficient, and aligns perfectly with React's design principles.

When to Use useMemo for Derived State

Now, you might be thinking: what if the calculation for derived state is expensive? Re-running it on every render, even if the underlying dependencies haven't changed, could be a performance hit. This is precisely where useMemo comes in handy.

useMemo memoizes the result of a computation. It will only re-run the calculation if one of its dependencies changes. This gives you the best of both worlds: consistency and performance.

typescript
import React, { useState, useMemo } from 'react';

interface Product {
  id: string;
  name: string;
  price: number;
  selected: boolean;
}

interface ProductListProps {
  initialProducts: Product[];
}

function ProductList({ initialProducts }: ProductListProps) {
  const [products, setProducts] = useState<Product[]>(initialProducts);

  // \u2705 Derived state computed with useMemo for performance optimization
  const totalSelectedPrice = useMemo(() => {
    console.log('Recalculating total selected price via useMemo');
    return products.reduce((sum, p) => (p.selected ? sum + p.price : sum), 0);
  }, [products]); // Re-run only when 'products' array changes

  const toggleProductSelection = (id: string) => {
    setProducts(prevProducts =>
      prevProducts.map(p => (p.id === id ? { ...p, selected: !p.selected } : p))
    );
  };

  return (
    <div>
      <h2>Products</h2>
      <ul>
        {products.map(product => (
          <li key={product.id}>
            <input
              type="checkbox"
              checked={product.selected}
              onChange={() => toggleProductSelection(product.id)}
            />
            {product.name} - ${product.price.toFixed(2)}
          </li>
        ))}
      </ul>
      <h3>Total Selected Price: ${totalSelectedPrice.toFixed(2)}</h3>
    </div>
  );
}

// Usage example:
// <ProductList initialProducts={[
//   { id: '1', name: 'Laptop', price: 1200, selected: false },
//   { id: '2', name: 'Mouse', price: 25, selected: false },
//   { id: '3', name: 'Keyboard', price: 75, selected: false },
// ]} />

With useMemo, the console.log inside the calculation will only fire when the products array itself changes, not on every render caused by other state updates or parent re-renders. This is the optimal pattern for derived state that might be computationally intensive.

When is useState appropriate for values that seem derived?

There are situations where a value initially looks like derived state but actually needs to be managed with useState. This happens when the value can be overridden by user interaction or other external factors, effectively becoming an independent piece of state. This is often called controlled vs. uncontrolled components in React.

Consider a text input field with a default value. If you derive its value from a prop, it will reset every time that prop changes. But if the user types something, their input needs to be the new source of truth for the input's value, not the original prop. In this case, the value of the input becomes its own state, potentially initialized from a prop but then managed internally.

typescript
import React, { useState, useEffect } from 'react';

interface EditableFieldProps {
  initialValue: string;
  onSave: (newValue: string) => void;
}

function EditableField({ initialValue, onSave }: EditableFieldProps) {
  // \u2705 This 'value' is *not* derived state; it's local state that can be edited independently
  const [value, setValue] = useState(initialValue);

  // \u2705 Use useEffect here to *re-synchronize* with a new initialValue if the prop changes
  // This is a controlled component pattern where internal state mirrors external prop.
  useEffect(() => {
    setValue(initialValue);
  }, [initialValue]);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value);
  };

  const handleBlur = () => {
    if (value !== initialValue) {
      onSave(value);
    }
  };

  return (
    <input
      type="text"
      value={value}
      onChange={handleChange}
      onBlur={handleBlur}
    />
  );
}

// Usage example:
// function ParentComponent() {
//   const [name, setName] = useState("Sunil Band");
//   return (
//     <EditableField initialValue={name} onSave={setName} />
//   );
// }

Here, value is initialized from initialValue, but then it can diverge. The useEffect is crucial to update value if initialValue changes from the parent. This is a controlled component pattern, where the component's internal state is controlled by an external prop, but also allows internal modifications. The distinction is subtle but important: totalSelectedPrice was always a direct computation; EditableField's value is sometimes based on initialValue and sometimes based on user input.

Trade-offs and Gotchas

The primary trade-off with computing derived state during render (even with useMemo) is that the computation still happens. For very complex, deeply nested objects or massive arrays, repeatedly iterating through them can indeed impact performance. useMemo helps, but it's not a magic bullet if the dependencies themselves are changing frequently with new object references.

Another gotcha: be mindful of object identity in useMemo dependency arrays. If you pass an object or array literal [] or {} directly into a dependency array, it will create a new object on every render, invalidating the memoization. Always ensure your dependencies are stable references, or memoize them upstream if they are also derived.

Finally, avoid over-optimizing with useMemo. If a computation is trivial (e.g., item.length or a simple + operation), the overhead of useMemo (creating and storing the memoized value, checking dependencies) might outweigh the benefit. Profile your components before reaching for useMemo defensively.

Wrapping up

Understanding derived state is fundamental to writing performant and bug-free React applications. The simplest mental model is often the best: if a value can be computed from existing state or props, compute it directly during render. Only introduce useMemo if profiling shows that computation is a bottleneck. Resist the urge to use useState and useEffect for values that are purely derivative.

Go through your existing React components. Look for useState variables that are immediately followed by a useEffect that updates that state based on other state or props. That's your first candidate for refactoring. Remove the useState, remove the useEffect, and compute the value inline. See how much cleaner and more robust your code becomes. Your future self (and your team) will thank you.

More from the blog
Available for projectsReady to make something fun 🎈

Ready to build the next system?Wanna build something awesome together?

Currently accepting high-impact opportunities in frontend engineering and scalable web applications.Got a cool idea rattling around? Let's grab a virtual coffee and turn it into something people love. ☕