{"slug": "what-i-learned-intentionally-breaking-hydration-in-next-js", "title": "what i learned intentionally breaking hydration in next.js", "summary": "A developer intentionally broke hydration in a Next.js app to understand how it works internally. By causing mismatches with new Date(), typeof window, and Math.random(), they discovered that hydration relies on exact DOM matching and that React silently fixes mismatches in production. The exercise revealed that hydration is not a full re-render but a walk of existing DOM to attach fiber nodes.", "body_md": "i did something dumb last month. on purpose.\n\ni sat down, opened a next.js app, and tried to make hydration fail in every way i could think of. not because a bug forced me to. not because i was debugging something. just because i wanted to see it. understand it from the inside.\n\nand honestly? best few hours i've spent learning anything in a while.\n\nwhy i even did this\n\nyou know how you use something for months and you think you get it, but you don't really get it? hydration was that for me. i knew the surface-level thing: server renders HTML, client takes over, they gotta match. cool. got it. moving on.\n\nexcept i didn't get it. i just got the vibe of it.\n\nevery time i saw hydration mismatch, i'd ask claude, fix the immediate thing, feel vaguely annoyed, and move on. i never stopped to ask why that specific thing broke it. i was treating symptoms, not understanding the actual disease.\n\nso i decided to break it deliberately. if i caused the errors myself, i'd actually have to understand what i was doing.\n\nthe setup\n\nbasic next.js app. app router. a few pages. nothing fancy.\n\ni wasn't trying to build anything. i was trying to destroy something, carefully, so i could see what fell apart and why.\n\n**break #1: the obvious one - new Date() on render**\n\nthis is the classic. everyone's seen it.\n\n```\nexport default function Page() {\n  return <div>{new Date().toLocaleString()}</div>\n}\n```\n\nserver renders this at, say, 14:00:00. by the time react runs on the client and tries to reconcile, it's 14:00:01. the strings don't match. react screams.\n\nthing is, i knew this would happen. what i didn't think about was why react cares.\n\nhere's the thing: react isn't doing a full diff on the entire DOM after hydration. it's trusting that the server HTML is a valid starting point and it's just attaching event listeners and state to it. but if the content doesn't match, it doesn't know what to trust. it can't partially hydrate \"mostly correct\" HTML. it either matches or it doesn't.\n\nso it throws the warning, and in some cases actually re-renders the whole thing client-side. you just wasted your server render.\n\nwhat i actually learned: hydration isn't \"react rechecks everything.\" it's react skipping the DOM creation step entirely and just walking the existing DOM to attach fiber nodes. mismatch breaks that walk.\n\n**break #2: typeof window !== 'undefined' used wrong**\n\nthis one's sneaky. i see this pattern everywhere.\n\n``` js\nexport default function Page() {\n  const isClient = typeof window !== 'undefined'\n  return <div>{isClient ? 'client' : 'server'}</div>\n}\n```\n\nserver: window doesn't exist -> renders \"server\"\n\nclient: window exists -> renders \"client\"\n\nmismatch. every time.\n\nthe fix people reach for is useEffect:\n\n``` js\nconst [isClient, setIsClient] = useState(false)\nuseEffect(() => setIsClient(true), [])\n```\n\nbut here's what clicked for me when i broke it intentionally: the reason useEffect fixes it is that the first render on the client still returns false. it matches the server. then after hydration completes, useEffect fires, state updates, component re-renders. by that point, react is done hydrating and owns the DOM. it can do whatever.\n\nthat timing is doing a lot of work. it's not a hack. it's actually the intended mental model - first render matches server, then client takes over.\n\n**break #3: rendering random values**\n\n```\nexport default function Page() {\n  return <div data-id={Math.random()}>{Math.random()}</div>\n}\n```\n\ni expected the hydration warning. i got it. but what surprised me was opening the react devtools and watching what actually happened.\n\nreact renders the component on the client. it walks the server-rendered DOM. it finds the div. the text content doesn't match. instead of partially fixing it, it flags the whole subtree.\n\nin dev mode you get the warning. in production? react just silently fixes it by replacing the server content with the client render. no error. no nothing. you'd never know unless you were watching the network tab and noticed your carefully SSR'd HTML was being thrown away immediately.\n\nthat was kind of unsettling actually.\n\n**break #4: reading from localStorage on first render**\n\n``` js\nexport default function Page() {\n  const theme = localStorage.getItem('theme') || 'light'\n  return <div className={`theme-${theme}`}>hello</div>\n}\n```\n\nthis one doesn't just mismatches - it crashes. localStorage doesn't exist on the server. you get a reference error before react even gets a chance to complain about hydration.\n\nbut the lesson here was different. it made me think about what \"server\" actually means in this context. people say \"the server doesn't have localStorage\" like it's a gotcha fact. but why? because the server is node. node doesn't implement browser APIs. it's not a browser. it's just a JavaScript runtime that can render react components to a string.\n\nwhen you think about it that way, the rule becomes obvious instead of arbitrary. don't use browser APIs in code that runs on the server. window, document, localStorage, navigator - all of these are browser things. next.js might be doing SSR but the \"S\" is still a server.\n\n**break #5: conditional rendering based on props that differ server/client**\n\nthis one was more subtle.\n\n```\n// some parent component\n<UserCard showEmail={Math.random() > 0.5} />\n```\n\nthe prop itself is random. so the server might render the email field, the client might not (or vice versa). the child component is \"correct\" in isolation but the inputs it's receiving are inconsistent.\n\ni'd never thought about hydration errors being caused outside the component that shows the error. react points at the component that rendered the mismatched output, but the actual bug is in the parent.\n\nthis one burns people in the wild when they're doing things like passing Date.now() as a prop, or any calculation that produces different values between server and client execution.\n\nthe part that actually messed with my head\n\nat some point i started reading through how react's hydration works internally. like actually reading the source, not just blog posts.\n\nreact has this function hydrateRoot. when you call it, react starts a \"hydration pass\" where instead of creating new DOM nodes, it reads existing ones. it's walking your real DOM and building its fiber tree from the DOM, not to the DOM.\n\nbut here's the thing - it still runs your component functions. it still calls useState, useEffect setup (well, in terms of registering effects), all of it. it just doesn't write any DOM.\n\nexcept when there's a mismatch. then it has to write DOM. and it has to figure out what to write, because the existing DOM is wrong. which means in a bad case, hydration is actually more expensive than a fresh client render because it tries the fast path first, fails, and then falls back to the slow path anyway.\n\ni genuinely did not know this before i started breaking things.\n\n**break #6: using suppressHydrationWarning everywhere**\n\nokay this one was me being chaotic.\n\n```\n<div suppressHydrationWarning>\n  {Math.random()}\n</div>\n```\n\nthis is a real prop. it tells react \"i know this might not match, don't warn me.\" it's meant for timestamps, dynamic ids, things like that.\n\ni started slapping it everywhere to see what would happen. and the answer is: everything looks fine. no warnings. but the content is still wrong. react silently accepts the server content for those nodes and doesn't re-render them from the client. so if you're using this as a way to \"fix\" hydration mismatches on important content, you're not fixing anything. you're just hiding it.\n\nthe actual content on screen is whatever the server rendered. if that's stale, it stays stale until something triggers a re-render.\n\nwhat breaking things taught me that reading docs didn't\n\ndocs tell you the what. \"hydration mismatch occurs when server and client render different content.\" yeah, i knew that.\n\nbreaking things showed me the how and why:\n\nhydration is a read-then-attach operation, not a render-then-replace\n\nthe first client render must match the server render, always, no exceptions\n\nuseEffect is specifically useful here because it runs after hydration completes\n\nproduction hides a lot of pain that dev mode surfaces as warnings\n\nthe error often points to the symptom, not the cause\n\ni think there's something genuinely underrated about intentionally making things break. not finding bugs by accident. deliberately causing failures to understand what the system's failure modes are.\n\nit's a different kind of learning. you're not reading about edge cases. you're creating them.\n\none thing i'd try next\n\ni want to do the same thing with suspense + streaming SSR. intentionally break the boundary conditions, see when react flushes what, figure out the actual mental model instead of the \"here's when you'd use \" tutorial version.\n\nprobably gonna do it this week if i stop getting distracted by other things.\n\n(okkay i won't stop getting distracted, but i'll try.)", "url": "https://wpnews.pro/news/what-i-learned-intentionally-breaking-hydration-in-next-js", "canonical_source": "https://dev.to/heytechomaima/what-i-learned-intentionally-breaking-hydration-in-nextjs-1l6h", "published_at": "2026-06-30 03:40:14+00:00", "updated_at": "2026-06-30 03:49:02.949320+00:00", "lang": "en", "topics": ["developer-tools", "large-language-models"], "entities": ["Next.js", "React", "Claude"], "alternates": {"html": "https://wpnews.pro/news/what-i-learned-intentionally-breaking-hydration-in-next-js", "markdown": "https://wpnews.pro/news/what-i-learned-intentionally-breaking-hydration-in-next-js.md", "text": "https://wpnews.pro/news/what-i-learned-intentionally-breaking-hydration-in-next-js.txt", "jsonld": "https://wpnews.pro/news/what-i-learned-intentionally-breaking-hydration-in-next-js.jsonld"}}