DEV Community

Alejandro
Alejandro

Posted on

Part 3: Stop Syncing Props Into State (you probably don't need that useEffect)

Part 3 of the Modern React Patterns series

In the previous article (Part 2: Stop Fetching Data Inside useEffect (what modern React apps do instead)) we talked about one of the biggest reasons why React applications end up full of unnecessary Effects: fetching server data inside useEffect.
This time I want to cover another pattern that I still find suprisingly often during code reviews, even in mature codebases. Synchronizing props into local state, at first, it doesn't even look like a mistake. Actually, it feels like the obvious thing to do lije a parent passes some data, the child stores it in its own state and whenever the parent changes, the child updates its copy. Except that, in most cases, React never needed that second copy to exist.


The Pattern Everyone Writes at Some Point

If you've been writing React for a while, you've probably written something like this:

function UserProfile({user}){
    const [name, setname] = useState("");
    useEffect(() => {
        setName(user.name);
    }, [user]);
    return(
        <input
            value={name}
            onChange={(e) => setname(e.target.value)}
        />
    );
}
Enter fullscreen mode Exit fullscreen mode

I've written code like this many times. I've also reviewed dozens of components doing exactly the same thing. The interesting part is that it usually works; nothing crashes, there are no console warnings, the UI behaves correctly during development, and that's why this pattern survives for so long. The problem don't usually appear until the component grows.


Why It Feels Like the Right Solution

When you're new to React, this pattern makes perfect sense. You receive some data, you keep it in state and when the prop changes, you synchronize it. Coming from other UI frameworks, this mental model feels completely natural. The problem is that React already re-renders your component every time its props change. That mean the prop is alrady synchronized. By copying it into state, you've created two values that are supposed to represent exactly the same thing. Now React has to keep both in sync. Not because React needs it, but because we introduced the duplication ourselves.


Two Sources of Truth

One of the easiest ways to make a React component harder to reason about is introducing multiple sources of truth. Imagine this simpler example:

function UserCard({user}){
    const [displayName, setDisplayName] = useState("");
    useEffect(() => {
        setDisplayName(user.name);
    }, [user]);
    return <h2>{displayName}</h2>;
}
Enter fullscreen mode Exit fullscreen mode

There are now two values representing the user's name.

  • user.name
  • displayName

Ideally they should always contain exactly the same value. The problem is that react doesn't know that, they're just two independent pieces of data and every time one changes, something has to update the other. That's exactly what the Effect is doing. The funny thing is that the Effect isn't adding any business logic. It's simply compensating for the fact that we duplicated state. Whenever I review code nowadays, I try spot these situations quickly because they're ofter a sign that the component is becoming more complex than it needs to be.


The Bugs Usually don't Appear Immediately

This is probably the biggest reason why this pattern is so common. The first version works pèrfectly and then somebody adds a feature.
Maybe the input becomes editable.

<input
    value={displayName}
    onChange={(e)=> setDisplayName(e.target.value)}
/>
Enter fullscreen mode Exit fullscreen mode

Still fine but a few weeks later another developer instroduces automatic refetching or optimistic updates or polling or live updates through WebSockets. Suddenly the Effect starts running much more often. Now imagine the user is typing while new data arrives from the server. Should the Effect overwrite what the user is writing? Should it ignore the update or should it merge both values?. At this point there isn't an obvious answer anymore. Not because React is difficult but because the component now owns two independent copies of the some data.


The Component Is Solving the Wrong Problem

One thing I've noticed after working on larger React application is that many Effets don't exist because the business logic is complicated. They exist because the component is trying to synchronize data that didn't need to be duplicated in the first place. That's an important distinction.
The business rule here isn't complicated: "show the user's name". That's it. The complexity comes from maintaining two values that should never diverge. Once I started looking at Effects this way, I found myself deleting far more of them than writing new ones.


Most Components Don't Need Local State

If the component only needs to display the data it receives, the simplest solution is usually the correct one.

function UserCard({user}){
    return(
        <>
            <h2>{user.name}</h2>
            <p>{user.email}</p>
        </>
    );
}
Enter fullscreen mode Exit fullscreen mode

There's no local state or synchronization or Effect. Every render simply reflects whatever the parent passed down. That's already how React works. Very often, adding local state doesn't make the component more flexible. It simply gives React another value that now has to stay synchronized.


Derived Values Don't Need State Either

Antoher variation of the same problem looks like this:

const [isAdmin, setIsAdmin] = useState(false);
useEffect(() => {
    setIsAdmin(user.role === "admin");
}, [user]);
Enter fullscreen mode Exit fullscreen mode

Again, there are now two sources of truth. But the second one doesn't even add new information, i'ts entirely derived from the first. Whenever I see this pattern nowadays, I usually replace it with a simple variable.

const isAdmin = user.role === "admin";
Enter fullscreen mode Exit fullscreen mode

There's no Effect or state updates or extra render, just a value calculated during rendering. It's a tiny change, but after removing enough Effects like this, components become noticeably easier to understand.


Forms Are Usually the Exeption

Whenever someone says "don't sync props into state", the first example that comes up is forms. And that's fair. Forms are probably the most common case where creating local state is the right decision. imagine an edit profile page, the server sends this object:

{
    id: 1,
    name: "John",
    email: "john@example.com"
}
Enter fullscreen mode Exit fullscreen mode

The user changes the name to Johnny, but hasn't clicked Save yet. At the moment there are actually two different pieces of information:

  • the persisted user coming from the server
  • the draft the user is editing

Those are not the same thing anymore. The mistake isn't copying the prop into state, but trying to keep both synchronized forever. Those values have different responsibilities. One represents the current server state and the other represents temporary UI state. Once you look at it that way, the problem becomes much clearer.


Initialize State once Instead of Synchronizing It Forever

One thing I see surprisingly ofter is this:

function UserForm({user}){
    const [name, setName] = useState("");
    useEffect(() => {
        setName(user.name):
    }, [user]);
    ...
}
Enter fullscreen mode Exit fullscreen mode

The intention is understandable. Whenever a new user arrives, update the form. The problem is that every update to user resets the input
If the parent refetches data while the user is typing, their changes disappear. Instead, ask yourself a different question. Something like if you need to keep the form synchronized or do you need an initial value. Those are completely different requirements


Let React Reset the Component

One of my favorite solutions is also one of the simplest. Instead of synchronizing state yourself, let React mount a fresh component.

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

Now the component behaves naturally. The process looks like this:

Open User A --> A new form is created --> Open User B --> React destroys the old form and creaters another one
Enter fullscreen mode Exit fullscreen mode

As you can see, there's no synchronization Effect or manual reset or duplicated logic.
Using key intentionally like this is something I almost never did when I started with React, but nowadays it's often my first option.


Sometimes Local State Is the Right Tool

Reading this article, it might sound like local state is something to avoid. But that's not the point at all. Local state is great, it's duplicated state that's usually the problem. If the local state represents something different form the incoming prop, then everything is perfectly fine.
Some examples are:

  • editable forms
  • wizard steps
  • drag-and-drop interactions
  • temporary filters
  • undo/redo history
  • optimistic UI

All of these create state that intentionally diverges from the original prop. That's completely different from mirroring a prop just because we think we need to.


My personal rule

Nowadays, whenever I catch myself writing this:

useEffect(() => {
    setSomething(prop);
}, [prop]);
Enter fullscreen mode Exit fullscreen mode

I stop for a second.
Then I ask myself one question:
Why does this component need its own copy of this value?
Sometimes there's a good answer but most of the times there isn't. Usually I realize that the component only needs to read the prop. Or maybe the value can be derived during rendering or perhaps React can reset the component naturally by changing its key. That one question has probably saved me from writing dozens of unnecessary Effects.


Alternatives Before Reaching for useEffect

When I review React code now, these are usually the alternatives I consider first:
If the value can be calculated, derive it during render.

const isAdmin = user.role === "admin";
Enter fullscreen mode Exit fullscreen mode

If the component only displays data, read it directly from props.

<h2>{user.name}</h2>
Enter fullscreen mode Exit fullscreen mode

If the component needs a fresh state for a different entity, remount it with a different key.

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

And if the component genuinely needs a draft that diverges from the original data, then local state is absolutely the right choice. The important part is understanding why that local state exists.


Conclusion

Looking back, I don't think synchronizing props into state is a beginner mistake. I've seen it in production code written by experienced developers. Mostly because it works, until it doesn't. The issue isn't the Effect itself, it's that the Effect is often hiding a deeper problem, the component owns data it never needed to own. One thing I've learned over the years is that React code becomes much easier to reason about when every pierce of data has a single owner. As soon as multiple copies appear, synchronizition logic follws. And synchronizition logic almost always ends up inside a useEffect. Whenever I find myself writing an Effect that copies a prop into state, I now assume it's unnecessary until I can prove otherwise. Most of the time, deleting the Effect actually makes the component simpler.


Next article in this series

part 4: Event Handlers Arethe Better Place for Side Effects

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)



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 (0)