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:
selectedEmail
is now the single source of truthwe reduce the risk of
setSelectedEmail
and theemail
saved inside the newly createdconfirmDeleteProps.onConfirm
getting out of syncwe 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.