DEV Community

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

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)} />;
}

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 problems 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 means the prop is already 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>;
}

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 to spot these situations quickly because they're often 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 perfectly, and then somebody adds a feature. Maybe the input becomes editable:

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

Still fine. But a few weeks later, another developer introduces 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 same data.

The Component Is Solving the Wrong Problem

One thing I've noticed after working on larger React applications is that many Effects 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>
    </>
  );
}

There's no local state, no synchronization, no 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

Another variation of the same problem looks like this:

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

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

const isAdmin = user.role === "admin";

There's no Effect, no state updates, no 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 Exception

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" }

The user changes the name to "Johnny" but hasn't clicked Save yet. At that 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 often is this:

function UserForm({ user }) {
  const [name, setName] = useState("");
  useEffect(() => {
    setName(user.name);
  }, [user]);
  // ...
}

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: do 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} />

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

  1. Open User A โ†’ a new form is created
  2. Open User B โ†’ React destroys the old form and creates another one

As you can see, there's no synchronization, no Effect, no manual reset, no 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 from 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]);

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 time 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";
    
  • If the component only displays data, read it directly from props:

    <h2>{user.name}</h2>
    
  • If the component needs a fresh state for a different entity, remount it with a different key:

    <UserForm key={user.id} user={user} />
    
  • 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 piece of data has a single owner. As soon as multiple copies appear, synchronization logic follows. And synchronization 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 Are the 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.

Comments

No comments yet. Start the discussion.