React states can get out of hand quickly.
The impact of a complex state doesn’t always break a component right away, but over time, it can lead to:
-
becoming out of sync as new states or edge cases are added
-
increased complexity, making the component harder to understand and maintain
This is why it’s crucial to spot complex React states and learn to refactor them into clean, maintainable code. I believe it’s best done through examples, so this post is perfect for intermediate React developers looking to level up their state management skills.
As a general guideline, we often refer to the “Single Source of Truth” pattern, which really means storing the data you need in one place.
In this guide, we’ll walk through refactoring a real-world component, examining common anti-patterns, and transforming them into clean, efficient code. By the end, you’ll have practical knowledge you can apply to your own React projects.
The Problem
A common scenario in React applications is a subscribers table with delete functionality.
You’ll likely encounter something similar when building email list management, user management, or subscription systems.
Inside such a table, you can do many things, but for the sake of simplicity, let’s assume that the only feature we care about is this:
Once the user presses delete for a line inside the table, the onDelete function is called with the email for that line.
The SubscribersTable component initially looked like this:
const SubscribersTable: React.FC<{
subscribers: Subscriber[];
onDeleteSubscriber: (email: string) => void;
}> = ({ subscribers, onDeleteSubscriber }) => {
const [confirmDeleteProps, setConfirmDeleteProps] = React.useState<{
email: string;
onConfirm: () => void;
} | null>(null);
const [selectedEmail, setSelectedEmail] = React.useState<string>();
const handleDeleteClick = (email: string) => {
setSelectedEmail(email);
setConfirmDeleteProps({
email,
onConfirm: () => onDeleteSubscriber(email)
});
};
return (
<div>
<Table
data={subscribers}
onDelete={handleDeleteClick}
/>
{confirmDeleteProps && (
<Dialog
isOpen={true}
title="Confirm Unsubscribe"
message={`Are you sure you want to remove ${confirmDeleteProps.email} from your list?`}
onConfirm={() => {
confirmDeleteProps.onConfirm();
setConfirmDeleteProps(null);
setSelectedEmail(undefined);
}}
onCancel={() => {
setConfirmDeleteProps(null);
setSelectedEmail(undefined);
}}
/>
)}
</div>
);
};
I’ll give you some time to think about potential issues with the above component.
…
..
.
Let me give you a hint: one of the states is redundant.
…
..
.
Yep, it’s the email we clicked!
Repeated States
setSelectedEmail and setConfirmDeleteProps are essentially tracking the same thing: the email the user wanted to delete inside this table:
<Table
data={subscribers}
onDelete={handleDeleteClick}
/>
And it’s not only that we track this information twice. We also do it in a complex manner:
setConfirmDeleteProps({
email,
onConfirm: () => onDeleteSubscriber(email)
});
We create a new onConfirm function every time we initiate a delete action for a subscriber. Then, we would call this function when confirm is clicked inside the dialog:
<Dialog
isOpen={true}
title="Confirm Unsubscribe"
message={`Are you sure you want to remove ${confirmDeleteProps.email} from your list?`}
onConfirm={() => {
confirmDeleteProps.onConfirm();
setConfirmDeleteProps(null);
setSelectedEmail(undefined);
}}
...
/>
Instead of all this, we could simply:
-
record the subscriber email that was clicked inside the table, just as before
-
remove the confirmDeleteProps state
-
initiate delete with
selectedEmail
Simplified React State - Single Source of Truth
Let’s make these modifications to the SubscribersTable component:
const SubscribersTable: React.FC<{
subscribers: Subscriber[];
onDeleteSubscriber: (email: string) => void;
}> = ({ subscribers, onDeleteSubscriber }) => {
const [selectedEmail, setSelectedEmail] = React.useState<string | null>(null);
return (
<div>
<Table
data={subscribers}
onDelete={setSelectedEmail}
/>
<Dialog
isOpen={selectedEmail !== null}
title="Confirm Unsubscribe"
message={`Are you sure you want to remove ${selectedEmail} from your list?`}
onConfirm={() => {
onDeleteSubscriber(selectedEmail!);
setSelectedEmail(null);
}}
onCancel={() => setSelectedEmail(null)}
/>
</div>
);
};
This approach has several benefits:
-
selectedEmailis now the single source of truth -
we reduce the risk of
setSelectedEmailand theemailsaved inside the newly createdconfirmDeleteProps.onConfirmgetting out of sync -
we simplify the state management, improving the readability and simplicity of the code
Conclusion
Remember, the goal of state management isn’t just to make things work - it’s to make them work in a clear, maintainable, and extensible way.
The next time you’re working with React state, ask yourself: “Is this the simplest possible state that could work?” Often, as we’ve seen here, the answer is “no” - and that’s your cue to refactor.