You Might Not Need an Effect
has been live for, at the very least, a couple of years now. It has really helped! 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 (...)
}
-
Double rendering:
data
changes => new render =>useEffect
runs =>dynamicData
changes => new render. -
Synchronizing React state to React state is unncessary.
-
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!
Adjusting some state when a prop changes
hides the answer to this issue:
Important
Derive your state!
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's why
null
is handy as an initial value. -
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) } ... }
- No double rendering:
data
changes, and the actual component state is derived during render.
Note
Since you'll need to reinitializeState
after awaiting for onSave
's promise, you'll kinda end up with two rerenders - but definitely faster! Technically, they could at least be batched if the state lived in the same component.
-
Deriving state is simpler to reason about; no indirection.
-
Predictably and on-demand reset your dynamic/local data.