Table of contents
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 useEffect
s: 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:
data
scheduled to be updated => newrender
=>useEffect
runs =>dynamicData
scheduled 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
'srefetchInterval
strikes; 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
null
handy as an initial value.Use
data
as your source of truth, and updatedynamicData
through its state dispatcher function.Use
data
asinput
s'value
s.Use
setDynamicData
to update thosevalue
s.
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:
data
changes, and the actual component state is derived during render.๐Since you'll need toreinitializeState
after 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.