Skip to content

Instantly share code, notes, and snippets.

@MarcosNASA
Last active July 3, 2024 17:25
Show Gist options
  • Save MarcosNASA/1642467f761b2eb7db8bd414756b0db6 to your computer and use it in GitHub Desktop.
Save MarcosNASA/1642467f761b2eb7db8bd414756b0db6 to your computer and use it in GitHub Desktop.
Avoid `useEffect`s to resynchronize server/local state

The study case

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 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 changes => new render => useEffect runs => dynamicData changes => new render.

  2. Synchronizing React state to React state is unncessary.

  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:

Important

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's why null is handy as an initial value.

  3. 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)
      }
      ...
    }

The benefits

  1. 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.

  1. Deriving state is simpler to reason about; no indirection.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment