DEV Community

Alejandro
Alejandro

Posted on

Part 5: Stop Using Effects for State Resets

Part 5 and last of the Modern React Patterns series

Throughout this series, we've looked at several situations where useEffect is often used to coordinate state that didn't actually need coordinating. Derived values were recalculated inside Effects instead of during rendering. Server data was fetched inside Effects instead of being managed by a data-fetching layer. Props were copied into local state and then synchronized manually. User actions were converted into state changes just to trigger an Effect. The patern in this article is slightly different. This time, the state itself may be perfectly valid. The problem is that we're trying to reset it manually when the component's context changes. That's where I often see code like this:

useEffect(() => {
    setSelectedItem(null);
}, [userId]);
Enter fullscreen mode Exit fullscreen mode

or

useEffect(() => {
    setStep(0);
}, [productId]);
Enter fullscreen mode Exit fullscreen mode

or

useEffect(() => {
    setFormData(initialData);
}, [initialData]);
Enter fullscreen mode Exit fullscreen mode

All of these can be correct in some situations. But quite often, the component doesn't need to reset its state. It needs to be treated as a new component.


The State Belongs to the Component Instance

Let's start with a simple example. Imagine a chat application. The same message composer is used for different conversations:

function ChatPage([conversationId}){
    return(
        <MessageComposer
            conversationId={conversationId}
        />
    );
}
Enter fullscreen mode Exit fullscreen mode

Inside the composer, we have local state:

function MessageComposer({conversationId}){
    const[message, setMessage] = useState("");
    return(
        <textarea
            value={message}
            onChange={(event) => {
                setMessage(event.target.value);
            }}
        />
    );
}
Enter fullscreen mode Exit fullscreen mode

The user types a message and then they switch to another conversation. Next, the new conversation should have a new empty composer.
A common solution is to reset the state when the ID changes:

function MessageComposer({conversationId}) {
    const [message, setMessage] = useState("");
    useEffect(() => {
        setMessage("");
    }, [conversationId]);
    return (
        <textarea
            value={message}
            onChange={(event) => {
                setMessage(event.target.value);
            }}
        />
    );
}
Enter fullscreen mode Exit fullscreen mode

Again, it works, but the component is still the same React instance. We're keeping it alive and manually trying to make it behave like a new one. There's often a simpler way.


Use key When the Component Represents a New Entity

Instead of resseting the component internally, we can tell React that the identity has changed.

function ChatPage({conversationId}){
    return (
        <MessageComposer
            key={conversationId}
            conversationId={conversationId}
        />
    );
}
Enter fullscreen mode Exit fullscreen mode

Now, when conversationId changes, React sees a different key. The previous MessageComposer is removed and a new one is created. Its state start from the initial value again.

function MessageComposer ({conversationId}){
    const[message, setMessage] = useState("");
    return(
        <textarea
            value={message}
            onChange={(event)} => {
                setMessage(event.target.value);
            }}
        />
    );
}
Enter fullscreen mode Exit fullscreen mode

There's no Effect, no manual reset or synchronization logic. The component simply starts with a new state because it represents a new entity. This is one of those React features that I completely underestimated when I was learning the framework. I used to think of key mainly as something required when rendering lists. But keys are also how React determines thew identity of components. That makes them useful far beyond arrays.


A Key Is About Identity, Not Just Lists

When React renders a component, it doesn't only look at the component type. It also considers its position in the tree and its key. For example:

<Chat conversationId="one" />
Enter fullscreen mode Exit fullscreen mode

and then:

<Chat conversationId="two" />
Enter fullscreen mode Exit fullscreen mode

are still the same component type in the same position.
React can preserve its state. But:

<Chat
    key="one"
    conversationId="one"
/>
Enter fullscreen mode Exit fullscreen mode

Followed by:

<Chat
    key="two"
    conversationId="two"
/>
Enter fullscreen mode Exit fullscreen mode

represents a different identity. React can now discard the previous state and create a fresh instance. That's exactly what we want when the state belongs to a specific entity. The conversation has changed and the form should be new.


Forms Are a Good Example

This becomes especially useful with forms.
Imagine an edit page:

function UserPage({user}){
    return <Userform user){user} />;
}
Enter fullscreen mode Exit fullscreen mode

The form has local state:

function UserForm({user}){
    const [name, setName] = useState(user.name);
    const [email, setEmail] = useState(user.email);
    return(
        <>
            <input
                value={name}
                onChange={(event)} => {
                    setEmail(event.target.value);
                }}
            />
        </>
    );
}
Enter fullscreen mode Exit fullscreen mode

This is a reasonable design; the form owns a draft and the draft is allowed to differ form the server data. That's exactly the distinction we discussed in the previous article. The problem appears when we navigate from one user to another.
Without a key:

<UserForm user={user} />
Enter fullscreen mode Exit fullscreen mode

React may preserve the existing state. The component is still in the same position in the tree. So the form that was editing User A may continue to contain User A's local draft even though the user prop now represents User B.
A common fix is:

useEffect(() => {
    setName(user.name);
    setEmail(user.email);
}, [user]);
Enter fullscreen mode Exit fullscreen mode

But now we're back to synchronizing prop into state and the problems start appearing again. A user might be editing a form when a background refetch happens. The action goes like this:

The server object changes --> the effect runs --> The local draft gets overwritten
Enter fullscreen mode Exit fullscreen mode

The reset logic has now become part of the component's behaviour. Instead, when the form represents a completely different user, I usually prefer:

<UserForm
    key={user.id}
    user={user}
/>
Enter fullscreen mode Exit fullscreen mode

Now the state naturally belongs to the user being edited and then the user changes, the form is recreated.


This Is Different Form Resetting a Form After Saving

It's important not to overapply this pattern. There is a difference between "the user changed, so this is a new form" and "the user submitted the form, so I want to clear it".
The first is an identity change and a key may be a good solution and the second one is an event ans that usually belongs i the submit handler:

async function handleSubmit(event){
    event.preventDefault();
    await saveUser(formData);
    setName("");
    setEmail("");
}
Enter fullscreen mode Exit fullscreen mode

The fact that both situations involve "resetting state" doesn't mean they should be solved the same way.
The important question is what caused the reset.


Resseting State Can Be a Sign of a Component Boundary Problem

Sometimes the problem isn't that the state needs to be reset, the problem is that the component owns too much state.
For example:

function ProductPage({productId}){
    const [selectedColor, setSelectedColor] = useState(null);
    const [selectedSize, setSelectedSize] = useState(null);
    const [quantity, setQuantity] = useState(1);
    useEffect(() => {
        setSelectedColor(null);
        setSelectedSize(null);
        setQuantity(1);
    }, [productId]);
    return(
        // ...
    );
}
Enter fullscreen mode Exit fullscreen mode

This works, but the Effect is responsible for manually resetting every piece of local state whenever the product changes. As the component grows, the reset list grows with it. A new piece of state gets added. Someone forgets to add it to the Effect and now the new product open with state from the previous product. This is the kind of code that often works perfectly until the component becomes large enough. At that point, the reset Effect becomes a maintenance problem.


When key Is the Right Tool

The most useful I've found to think about this is simple: If the component represents a different entity, changing the key is often the cleanest way to reset its local state. For example:

<ProductEditor
    key={productId}
    productId={productId}
/>
Enter fullscreen mode Exit fullscreen mode

When productId changes, the editor is no longer editing the same thing. A new component instance makes sense.
The same idea works for:

  • switching between users
  • changing conversations
  • navigating between documents
  • opening a form for a different record
  • changing the current step of an independent workflow

In all of these cases, the state belongs to the entity being displayed. A new entity should usually get a new state-


When You Should Not Use key

This doesn't mean that key should become a replacement for every reset Effect. Sometimes the component is still the same component and the state be preserved.
For example:

<Tabs>
    <Profile />
    <Settings />
</Tabs>
Enter fullscreen mode Exit fullscreen mode

If the user switches between tabs, you might intentionally want each tab to preserve its state. In that case, destroying and recreating the component would be the wrong behavior. The same applies when only part of the state needs to change. If the user selects a new filter but you want to preserve their sort order, pagination or other local preferences, remounting the entire component may reset too much. This is why I don't think of key as a generic "reset button". It is primarily about identity.


My Personal Rule

The question it's not if you can use a key to reset this component, i'ts if this represent a different instance of the same thing. And if the answers is yes, key is often a good fit.


The Pattern I Try to Avoid

The pattern I now question is this:

function Component({id}){
    const [state, setState] = useState(initialState);
    useEffect(() => {
        setState(initialState);
    }, [id]);
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Before keeping the Effect, I ask whether the component should simple be recreated:

<Component
    key={id}
    id={id}
/>
Enter fullscreen mode Exit fullscreen mode

Sometimes the answer is yes and sometimes it isn't. The important part is understanding why the state is being reset instead of automatically reaching for and Effect.


In general terms: use state for information that should persist within a component instance and use key when the component should become a new instance.


Conclusion

After working on larger React applications, I've become much more suspicious of Effects whose only purpose is to reset local state. Not because they are always wrong but because they often indicate that React's component identity isn't matching the way we think about the data. If a component moves from editing User A to editing User B, those are often two different component instances from a state perspective.

If a form changes from Product A to Product B, its draft probably shouldn't be manually synchronized with an Effect. An if a component is still representing the same entity, then its state probably shouldn't be reset just because some unrelated prop changed. And that leads us to the general rule I said before.


Finally

Across these five artiles, we've looked at some of the most common situations where useEffect is used as a general-purpose tool:

  • derived values that can be calculated during render
  • data fetching that belongs in a data-fetching layer
  • props that don't need to be synchronized into state
  • user actins that belong in event handlers
  • state resets that can often be handled through component identity

The common thread isn't that useEffect is bad. It's that Effects should have a clear reason to exist.


Previous Articles in This Series

Part 1: Why I Rarely Use useEffect Anymore (and what I use instead)
Part 2: Stop Fetching Data Inside useEffect (what modern React apps do instead)
Part 3: Stop Syncing Props Into State
Part 4: Event handlers are usually the better place for side effect




Thanks for reading!! If you're looking for help with an existing project or need someone to solve a tricky frontend/backend issue, you can also find my freelance profiles through my Dev.to profile.

Top comments (1)

Collapse
 
frank_signorini profile image
Frank

How do you handle edge cases where effects are still necessary for state resets, curious to hear your thoughts on this. Would love to swap ideas on alternative approaches.