The Unnecessary Sync: Props to Local State
Many React developers, even those working on mature codebases, fall into a common trap: synchronizing props into local state using useEffect. This pattern, where a parent component passes data down as props, and the child component immediately copies that data into its own internal state, seems intuitive. The child component might then update this local state, thinking it's managing its own data. However, in the vast majority of cases, this extra layer of state is entirely redundant and introduces unnecessary complexity.
Consider a scenario where a parent component passes a userProfile object to a child component. The child component then decides to create its own state variable, say localUserProfile, and uses useEffect to copy the userProfile prop into it whenever the prop changes. The child might then proceed to modify localUserProfile directly. This approach creates a disconnect: the child is working with a stale copy of the data, and the parent has no direct way of knowing about the child's modifications unless the child explicitly passes them back up. This is a classic anti-pattern that React's design often makes unnecessary.

Why the Sync is Usually Wrong
React's declarative nature and unidirectional data flow are powerful tools. When a parent passes a prop, that prop is a direct representation of the parent's state. If the parent's state changes, the prop will automatically update in the child. If the child needs to display or use that data, it can do so directly from the prop. If the child needs to modify that data, it should ideally trigger a callback function passed down from the parent, which then updates the parent's state. This mechanism ensures that the data remains consistent and predictable across the component tree.
The problem with syncing props to local state is that it breaks this single source of truth. The child component ends up with two versions of the same data: the prop itself and its local copy. When the prop updates, the useEffect fires, updating the local state. But what if the child modifies its local state directly? This modification is isolated and invisible to the parent. The next time the prop updates, the useEffect will overwrite the child's local changes, potentially losing important data. This creates a state synchronization nightmare, often requiring complex logic to reconcile the two states.
The Correct Approach: Direct Prop Usage and Callbacks
Instead of creating local state for props, developers should leverage props directly. If a child component needs to display data, it should simply use the prop. If the child needs to modify that data, it should call a function passed down from the parent. This function, defined in the parent, will be responsible for updating the parent's state, which in turn will re-render the child with the new prop value. This is the essence of lifting state up and is a fundamental React pattern.
Consider the userProfile example again. If the child needs to display the user's name and email, it simply accesses props.userProfile.name and props.userProfile.email. If the user needs to update their email address, the child component would have an input field whose onChange handler calls a function like handleEmailChange(newEmail). This handleEmailChange function would be defined in the parent component and passed down as a prop: <ChildComponent onEmailChange={handleEmailChange} />. The parent's handleEmailChange function would then update the parent's state, which holds the userProfile object.
This pattern is cleaner, more predictable, and aligns perfectly with React's core principles. It eliminates the need for useEffect to synchronize state, reducing boilerplate code and potential bugs. It ensures that there is always a single, authoritative source of truth for the data.
When Might You *Actually* Need Local State for Props?
There are specific, albeit less common, scenarios where deriving state from props might be necessary. One such case is when the child component needs to maintain its own internal state that is initialized by a prop but can then evolve independently. For instance, a form field might be initialized with a default value from a prop. If the user edits this value, the child component might want to manage this intermediate, unsaved state locally before committing it back to the parent. In such cases, it's crucial to initialize the local state correctly from the prop and then handle updates to the prop carefully. A common pattern here is to use the prop as the initial value and then rely on user interactions to manage the state, rather than using useEffect to constantly sync.
Another edge case is when the prop represents an initial configuration that should only be applied once. If subsequent changes to the prop should be ignored by the child, you could use a prop to set an initial local state, but then detach the local state from the prop. This requires careful management of the component's lifecycle and understanding when updates should or should not propagate.
However, for the vast majority of use cases, especially those involving displaying or editing data that originates from a parent, direct prop usage and callback functions are the superior and simpler solution. Developers should question the need for useEffect to sync props into state, as it often indicates a misunderstanding of React's data flow or an over-engineered solution.
