Stable effect dependencies

Depend on primitives, read callbacks via useEffectEvent, init once.

React docsreact.dev
Reference: Initializing the application
You Might Not Need an EffectOpen on react.dev ↗

From vercel-react-best-practices: rerender-dependencies and 4 more.

Listener churn on every render

Dispatch. Both should log the current count.

count 0
Handler in depsBad
addEventListener
0
removeEventListener
0
Handler saw
–
Not dispatched yet
useEffect(subscribe, [event, handler])
useEffectEventGood
addEventListener
0
removeEventListener
0
Handler saw
–
Not dispatched yet
useEffect(subscribe, [event])
function useWindowEvent(event, handler) {
  const onEvent = useEffectEvent(handler) // React 19.2+: always the latest handler
  useEffect(() => {
    const listener = (e) => onEvent(e)
    window.addEventListener(event, listener)
    return () => window.removeEventListener(event, listener)
  }, [event]) // not onEvent: it is a new function every render
}

// Older React: the same idea with a ref
function useWindowEvent(event, handler) {
  const handlerRef = useRef(handler)
  useEffect(() => { handlerRef.current = handler }, [handler])
  useEffect(() => {
    const listener = (e) => handlerRef.current(e)
    window.addEventListener(event, listener)
    return () => window.removeEventListener(event, listener)
  }, [event])
}

Which edit reconnects?

Edit the name, re-render, then switch user.

user.id u_1
[user]Bad
Connections opened
0
Same-id reconnects
0
    useEffect(() => connect(user.id), [user])
    [user.id]Good
    Connections opened
    0
    Same-id reconnects
    0
      useEffect(() => connect(user.id), [user.id])

      function Presence({ user }) {
        useEffect(() => {
          const channel = connect(user.id)
          return () => channel.close()
        }, [user.id]) // a new object with the same id is not a new user
      }

      App init: once per load

      Remount a few times, with and without Strict Mode.

      useEffect(init, [])Bad
      <App />
      checkAuthToken()
      0
      Mounts
      0
        useEffect(() => checkAuthToken(), [])
        Module-level didInitGood
        <App />
        checkAuthToken()
        0
        Mounts
        0
          if (didInit) return

          let didInit = false // module scope: one per page load
          
          function App() {
            useEffect(() => {
              if (didInit) return
              didInit = true
              loadFromStorage()
              checkAuthToken()
            }, [])
          }
          
          // Or run it in the entry module, before anything renders:
          if (typeof window !== 'undefined') {
            loadFromStorage()
            checkAuthToken()
          }

          Why it matters, pros and cons

          Why it matters

          A dependency array is a restart list. New objects each render restart everything.

          Pros

          • One subscription per real change.
          • Callback stays current, no stale closure.
          • Init runs once, same in dev and prod.

          Cons

          • A value that should restart the effect belongs in deps.
          • useEffectEvent needs React 19.2+ and stays inside effects.
          • A module flag outlives sign-out and tests; reset it.