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:
Store the filtered list.
Store the total price.
Store the completed count.
Store whether the current form is valid, just for convenience.
But as you keep writing, the component ends up like this:
One copy of raw data.
Another copy of derived data.
Each piece of data has an effect behind it to keep them in sync.
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:
fullName = firstName + ' ' + lastNamecompletedCount = todos.filter(t => t.done).lengthvisibleItems = items.filter(i => i.visible)totalPrice = cartItems.reduce(...)
These values share a common trait:
They are not independent facts.
They can all be computed from existing props or state.
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:
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:
fullNameis alreadyfirstName + lastName.And now you've added an extra state.
And an extra effect to maintain it.
Then every time props change, the component renders the old value first, then runs the effect, calls setState, and re-renders with the new value.
This is a classic case of 'writing something that is just one expression as two lifecycles'.
The refactored version is actually just one line:
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:
firstNameandlastNameare the source data.fullNameis the result, not the source data.
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
Current text in an input field.
Currently selected tab.
Raw list returned from a request.
Whether the user has expanded a popup.
Because these are user input, server results, or current UI state — they are facts themselves.
Should not store
Filtered list.
Count under certain conditions.
Display text concatenated from two fields.
Whether it's empty, whether it matches conditions, whether it needs highlighting.
Because these can all be derived from existing facts, no need to store a separate copy.
A more straightforward way to put it:
state stores facts
variables store computed results
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:
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:
filteredProductsis not independent state.It is just a function result of
products + keyword.By trying to 'cache' it, you instead introduce extra rendering and synchronization overhead.
A more reasonable version:
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:
productsandkeywordchange;filteredProductsnaturally changes along with them.
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:
First ask: Can I compute directly?
Then ask: Is this computation heavy enough to be worth caching?
Then ask: Is it because downstream memoized components need a stable reference?
For example:
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:
const sortedProducts = useMemo(() => {
return [...products]
.filter(p => p.visible)
.sort((a, b) => a.price - b.price);
}, [products]);might be meaningful, because:
It involves array filtering and sorting;
If the data is large and parent re-renders are frequent, repeated computation might have a cost.
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.
const [items, setItems] = useState<Product[]>([]);
const [count, setCount] = useState(0);
useEffect(() => {
setCount(items.length);
}, [items]);The problem is:
You are now maintaining two copies of information that should be one.
One day someone modifies
count, or a certain path doesn't go through this effect, and the UI becomes incorrect.
But if you write directly:
const count = items.length;This problem never occurs.
2. Chain side effects
This is the most nauseating smell in many large components later on:
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:
const filtered = ...
const sorted = ...
const count = sorted.lengthThis 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:
First render with the old value.
The effect runs after commit.
Call setState.
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:
Which one is the actual source data?
Why is there a delay here?
Why does a display value have its own state?
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:
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:
Previous scroll position.
Intermediate value during an animation.
Snapshot of the user's last submission.
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:
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:
Logic is correct.
But the whole component feels like it's 'maintaining a caching system'.
After refactoring:
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:
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:
Only the real source data remains as state.
Everything else is readable, traceable expressions.
Data relationships form a single chain, not effects passing the baton to each other.
9. Key takeaways from this article
State should store 'facts', not 'formula results'.
If a value can be computed directly from props/state, it usually should not go into state.
Using effects to synchronize derived state creates extra renders, chain updates, and desynchronization risks.
useMemois an optimization tool, not a default pattern; compute directly first, then decide whether to cache.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.
Follow on Google
Add HeyBinyang as a preferred source on Google
If you'd like to keep finding my updates through Google, you can mark this site as a preferred source and make it easier to spot in relevant reading flows.
SHARE
Share
Share this article.