use client doesn't mean browser-only
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:
- Server -- Node.js runs your render function, produces HTML.
useStateinitializer runs. Dependency arrays get evaluated. ButuseEffectcallbacks don't run. - Browser -- HTML shows up first. Then hydration kicks in. Event listeners attach. State connects.
useEffectcallbacks 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?
| Location | Server runs it? | window safe? |
|---|---|---|
| Top-level variable | Yes | No |
useState initializer | Yes | No |
useEffect dependency array | Yes | No |
useEffect callback | No | Yes |
| Event handler | No | Yes |
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.