Skip to main content

Command Palette

Search for a command to run...

Never `useEffect` again or YOU_WILL_BE_FIRED

Never-ish

Updated
3 min read
Never `useEffect` again or YOU_WILL_BE_FIRED
M

Software engineer @Compound

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

  1. Double rendering: data scheduled to be updated => new render => useEffect runs => dynamicData scheduled to be updated => new render.

  2. Synchronizing React state to React state is unnecessary.

  3. Dynamic/local data may be unexpectedly overwritten when/if static/props data changes unexpectedly. Imagine your user is filling a form and your useQuery's refetchInterval 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:

💡
Derive your state!

This is how you'd do it:

  1. Initialize your local/dynamic state as null.

     const Edit = ({ data: staticData }) => {
       const [dynamicData, setDynamicData] = useState(null)
       ...
     }
    
  2. 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.

  3. Use data as your source of truth, and update dynamicData through its state dispatcher function.

    • Use data as inputs' values.

    • Use setDynamicData to update those values.

    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` */      
      )
    }
  1. 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

  1. No double rendering: data changes, and the actual component state is derived during render.

    🗒
    Since you'll need to reinitializeState after awaiting for onSave'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.
  2. Deriving state is simpler to reason about; no indirection.

  3. Predictably and on-demand reset your dynamic/local data.

L

I was doing this in all my React apps, now I know how to avoid this mistake. Thanks for sharing this!

1