use client doesn't mean browser-only

nextjsreactssr

I thought use client meant "this only runs in the browser."

It doesn't. It means "this component needs hydration." The server still renders it first.


The crash that taught me

This broke during next build:

'use client'
 
function MyComponent() {
  useEffect(() => {}, [window])  // ReferenceError: window is not defined
  return <div />
}

This worked fine:

'use client'
 
function MyComponent() {
  useEffect(() => {
    console.log(window.scrollX)  // no crash
  }, [])
  return <div />
}

Same file. Same use client. One crashes, one doesn't.


What's actually happening

A use client component lives two lives:

  1. Server -- Node.js runs your render function, produces HTML. useState initializer runs. Dependency arrays get evaluated. But useEffect callbacks don't run.
  2. Browser -- HTML shows up first. Then hydration kicks in. Event listeners attach. State connects. useEffect callbacks run now.

The dependency array [window] is evaluated during step 1. Node.js hits window, doesn't find it, throws.

The callback () => { console.log(window.scrollX) } is just a function definition during step 1. Node.js sees it but never calls it. It only runs in step 2.


Where is window safe?

LocationServer runs it?window safe?
Top-level variableYesNo
useState initializerYesNo
useEffect dependency arrayYesNo
useEffect callbackNoYes
Event handlerNoYes

One escape hatch works at the top level:

const isBrowser = typeof window !== 'undefined'

typeof on an undefined variable returns "undefined" instead of throwing. That's a JavaScript thing, not a React thing.


The mental model

use client doesn't mean "skip the server." It means "send JavaScript to the browser so this component can hydrate."

useEffect is the only place guaranteed to run exclusively in the browser. Everything else -- function body, useState initializer, dependency arrays -- runs on the server first.


References