Tech0 views

React Hooks: How to Refactor Computed Properties and Derived State

When writing React, the easiest way to bloat a component is not network requests or animations, but 'storing a bit too much state'.

At first, you just think it's more convenient to write it this way:

But as you keep writing, the component ends up like this:

In the end, the component may still work, but it has a very typical feel: lots of state, little computation, and a pile of effects.

Next, let's start with the 'derived state' problem and gradually break down the design issues and solutions.

1. What is 'derived state'?

First, look at a few common variables:

These values share a common trait:

Such values are called derived state, which can also be understood as 'computed properties' in a component.

The React team has always emphasized a principle: > If a value can be computed from the current props or state, then you usually should not store it as separate state.

Why?

Because once you store it in state, you have to maintain that it is always in sync with the original state. And the place where people most often make mistakes is exactly 'synchronizing two copies of data that should only be one'.

2. The most classic anti-pattern: using useEffect to synchronize derived state

Let's look at a very common example:

tsx
function UserCard({
  firstName,
  lastName,
}: {
  firstName: string;
  lastName: string;
}) {
  const [fullName, setFullName] = useState('');

  useEffect(() => {
    setFullName(`${firstName} ${lastName}`);
  }, [firstName, lastName]);

  return <div>{fullName}</div>;
}

The problem with this code is not that it throws an error, but that it does something completely unnecessary:

This is a classic case of 'writing something that is just one expression as two lifecycles'.

The refactored version is actually just one line:

tsx
function UserCard({
  firstName,
  lastName,
}: {
  firstName: string;
  lastName: string;
}) {
  const fullName = `${firstName} ${lastName}`;
  return <div>{fullName}</div>;
}

This kind of refactoring looks like 'simplifying code', but in essence it restores the correct data modeling:

It should not have its own lifecycle.

3. How to decide if a value should be stored: see if it is an 'independent fact'

Here's a very practical rule: > A value is only worth storing in state if it is an 'independent fact'. > If it is just a transformation of other state, then it should be an expression.

For example:

Should store

Because these are user input, server results, or current UI state — they are facts themselves.

Should not store

Because these can all be derived from existing facts, no need to store a separate copy.

A more straightforward way to put it:

Once this boundary is clear, many components will slim down immediately.

4. A real refactoring: Stop storing two copies for list filtering

First, look at a typical bad code example:

tsx
function ProductList({
  products,
  keyword,
}: {
  products: Product[];
  keyword: string;
}) {
  const [filteredProducts, setFilteredProducts] = useState<Product[]>([]);

  useEffect(() => {
    const next = products.filter(product =>
      product.name.toLowerCase().includes(keyword.toLowerCase())
    );
    setFilteredProducts(next);
  }, [products, keyword]);

  return (
    <ul>
      {filteredProducts.map(product => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

Same problem as before:

A more reasonable version:

tsx
function ProductList({
  products,
  keyword,
}: {
  products: Product[];
  keyword: string;
}) {
  const filteredProducts = products.filter(product =>
    product.name.toLowerCase().includes(keyword.toLowerCase())
  );

  return (
    <ul>
      {filteredProducts.map(product => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

At this point, the data flow is very clear:

No intermediate synchronization layer, no effect, no risk of 'missing an update'.

5. What about useMemo? When to use it and when not to overuse it?

The React team emphasizes: Compute directly first, and only consider useMemo when the computation is truly expensive, or when you need a stable reference.

In other words, the priority should be:

  1. First ask: Can I compute directly?

  2. Then ask: Is this computation heavy enough to be worth caching?

  3. Then ask: Is it because downstream memoized components need a stable reference?

For example:

tsx
const fullName = `${firstName} ${lastName}`;

There is absolutely no need for useMemo. Because it has almost no cost; wrapping it in memo only adds mental overhead.

But something like this:

tsx
const sortedProducts = useMemo(() => {
  return [...products]
    .filter(p => p.visible)
    .sort((a, b) => a.price - b.price);
}, [products]);

might be meaningful, because:

But also note that there is a common misconception in the community about useMemo: treating all array operations as 'expensive computations'.

The Developer Way's perspective is worth referencing — often the truly expensive part is not the JS filtering itself, but the re-rendering of the entire subtree. So useMemo should be a precision tool, not a default button.

So the conclusion is: > useMemo is for heavy computations and stable references, not for all derived values.

6. Three types of problems most easily caused by derived state

1. State desynchronization

This is the most direct one.

tsx
const [items, setItems] = useState<Product[]>([]);
const [count, setCount] = useState(0);

useEffect(() => {
  setCount(items.length);
}, [items]);

The problem is:

But if you write directly:

tsx
const count = items.length;

This problem never occurs.

2. Chain side effects

This is the most nauseating smell in many large components later on:

tsx
useEffect(() => {
  setFiltered(...);
}, [items, keyword]);

useEffect(() => {
  setSorted(...);
}, [filtered]);

useEffect(() => {
  setCount(sorted.length);
}, [sorted]);

This kind of code looks 'layered', but in essence it's manually building a data processing pipeline with effects. eslint-plugin-react-you-might-not-need-an-effect even has specific rules to prevent this, such as no-derived-state and no-chain-state-updates.

The correct approach is to restore it to an expression chain:

tsx
const filtered = ...
const sorted = ...
const count = sorted.length

This is the data flow that React components should have.

3. Extra render cycle, and logic becomes more convoluted

This is something many people don't feel at first, but it hurts over time.

Using effects to synchronize derived state usually goes like this:

  1. First render with the old value.

  2. The effect runs after commit.

  3. Call setState.

  4. Then re-render with the new value.

In other words, a task that could have been done in one go during render is split into two rounds.

This not only wastes performance, but more importantly makes it harder for people reading the code to understand:

7. When is 'derived state' actually worth storing separately?

At this point, it's easy to swing to the other extreme: should all derived state absolutely never be stored in state?

Not necessarily.

There are several scenarios where storing separately is reasonable.

1. It comes from props, but will later be modified independently by the user

For example, an editor form:

tsx
function Editor({ initialTitle }: { initialTitle: string }) {
  const [title, setTitle] = useState(initialTitle);
  // The user will manually modify title later
}

Here, although title initially comes from props, it has since become the component's own mutable state. It is no longer just a pure derived value.

But you still need to be careful in this scenario: don't let it be controlled by props while also modifying it freely internally, otherwise you can easily fall into the 'two sources of truth' hell.

2. You need to record 'history' or 'transition process'

For example:

Although these values are related to other state, they are no longer just the 'computed result of current values' but an independent fact with a time dimension.

3. You intentionally want to cache a result, but the caching itself carries business semantics

For example, local drafts, snapshots, undo stack. Here 'caching' is not a performance measure but a business requirement.

So the real point of this article is not 'derived state must never be stored', but: > If you only want to display a value that can be computed on the fly, don't store it. > If you store it because it already has its own business semantics, then consider storing it.

8. A complete refactoring example: From 'state management' back to 'data expression'

First, look at the bad code:

tsx
function TodoPanel({ todos, filter }: Props) {
  const [visibleTodos, setVisibleTodos] = useState<Todo[]>([]);
  const [completedCount, setCompletedCount] = useState(0);
  const [remainingCount, setRemainingCount] = useState(0);

  useEffect(() => {
    const next = todos.filter(todo => {
      if (filter === 'all') return true;
      if (filter === 'done') return todo.done;
      return !todo.done;
    });

    setVisibleTodos(next);
  }, [todos, filter]);

  useEffect(() => {
    setCompletedCount(visibleTodos.filter(todo => todo.done).length);
    setRemainingCount(visibleTodos.filter(todo => !todo.done).length);
  }, [visibleTodos]);

  return (
    <div>
      <div>已完成:{completedCount}</div>
      <div>未完成:{remainingCount}</div>
      <ul>
        {visibleTodos.map(todo => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
    </div>
  );
}

This kind of code is very similar to the reality in many business projects:

After refactoring:

tsx
function TodoPanel({ todos, filter }: Props) {
  const visibleTodos = useMemo(() => {
    return todos.filter(todo => {
      if (filter === 'all') return true;
      if (filter === 'done') return todo.done;
      return !todo.done;
    });
  }, [todos, filter]);

  const completedCount = useMemo(() => {
    return visibleTodos.filter(todo => todo.done).length;
  }, [visibleTodos]);

  const remainingCount = visibleTodos.length - completedCount;

  return (
    <div>
      <div>已完成:{completedCount}</div>
      <div>未完成:{remainingCount}</div>
      <ul>
        {visibleTodos.map(todo => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
    </div>
  );
}

If the data volume is not large, you can simplify further and even drop the second useMemo:

tsx
const completedCount = visibleTodos.filter(todo => todo.done).length;
const remainingCount = visibleTodos.length - completedCount;

The biggest change in this version of the code is not that it's 'shorter', but:

9. Key takeaways from this article

  1. State should store 'facts', not 'formula results'.

  2. If a value can be computed directly from props/state, it usually should not go into state.

  3. Using effects to synchronize derived state creates extra renders, chain updates, and desynchronization risks.

  4. useMemo is an optimization tool, not a default pattern; compute directly first, then decide whether to cache.

  5. Truly worth storing separately as 'derived values' are often no longer pure derivations, but independent facts with business semantics, historical semantics, or user editing semantics.

SHARE

Share

Share this article.