Never `useEffect` again or YOU_WILL_BE_FIRED
Never-ish

The study case
You Might Not Need an Effect was released, at the very least, a couple of years ago now. It has really helped! Dan did an amazing job and collected a bunch of feedback on Twitter while writing the whole section around `effect`s.
However, I keep seeing at least one incorrect and widely-spread usage of useEffects: server/local state synchronization.
const Edit = ({ data, onSave }) => {
const [dynamicData, setDynamicData] = useState(data)
useEffect(() => {
setDynamicData(data) // Don't do this!
}, [data])
return (...)
}
The downsides
Double rendering:
datascheduled to be updated => newrender=>useEffectruns =>dynamicDatascheduled to be updated => newrender.Synchronizing React state to React state is unnecessary.
Dynamic/local data may be unexpectedly overwritten when/if static/props data changes unexpectedly. Imagine your user is filling a form and your
useQuery'srefetchIntervalstrikes; it would overwrite all the dynamic/local data your user may have input!
The fix
Adjusting some state when a prop changes hides the answer to this issue:
This is how you'd do it:
Initialize your local/dynamic state as
null.const Edit = ({ data: staticData }) => { const [dynamicData, setDynamicData] = useState(null) ... }Derive the actual component state:
const Edit = ({ data: staticData, onSave }) => { const [dynamicData, setDynamicData] = useState(null) const data = dynamicData ?? staticData ... }This allows you to keep your state minimal. The premise is using the local/dynamic data only after there has been interaction, and otherwise use the props/static data. That makes
nullhandy as an initial value.Use
dataas your source of truth, and updatedynamicDatathrough its state dispatcher function.Use
dataasinputs'values.Use
setDynamicDatato update thosevalues.
const Edit = ({ data: staticData, onSave }) => {
const [dynamicData, setDynamicData] = useState(null)
const data = dynamicData ?? staticData
return (
/** (Mostly) Always use `data` as your source of truth, and update `dynamicData` */
)
}
Don't forget to clear local/dynamic data as needed.
const Edit = ({ data: staticData, onSave }) => { const [dynamicData, setDynamicData] = useState(null) const data = dynamicData ?? staticData const reinitializeState = () => { setDynamicData(null) } const handleSave = () => { onSave(data).then(reinitializeState) } return ( /** (Mostly) Always use `data` as your source of truth, and update `dynamicData` */ ) }
The benefits
No double rendering:
datachanges, and the actual component state is derived during render.🗒Since you'll need toreinitializeStateafter awaiting foronSave's promise to resolve, you'll kinda end up with two renders - but definitely faster! Technically, they could at least be batched if really concerned.Deriving state is simpler to reason about; no indirection.
Predictably and on-demand reset your dynamic/local data.


