Docs/Runtime & Infrastructure/Hydration

Runtime & Infrastructure

Hydration

Hydration — Runtime & Infrastructure documentation for BCP Framework.

Last updated: 2026-08-29T01:11:41

Hydration and deterministic rendering

BCP uses server-side rendering for the initial HTML and React hydration in the browser. The server-rendered tree and the first client-rendered tree must produce the same element attributes and text.

Development transform parity

BCP Framework 0.1.21 fixes a development hydration mismatch caused by the server and client using different JSX transform semantics.

A common trigger is a multiline quoted JSX attribute:

TypeScript
export default function Page() {
    return (
        <main
            className="
                min-h-screen
                bg-white
                text-slate-950
            "
        >
            Hello
        </main>
    );
}

Before 0.1.21, development SSR loaded TSX through the server runtime while the client React Refresh path transformed the same module with @babel/preset-react. Babel's JSX transform normalizes whitespace in multiline quoted JSX attributes, so the client could receive a value such as:

text
" min-h-screen bg-white text-slate-950 "

while SSR produced the original multiline value:

text
"\n                min-h-screen\n                bg-white\n                text-slate-950\n            "

Those class lists are visually equivalent to CSS but they are different JavaScript strings, so React reports a hydration mismatch.

BCP 0.1.21 keeps Babel in the development pipeline for TypeScript stripping and React Refresh registration, but Babel no longer compiles JSX. JSX is handed to esbuild with the development JSX runtime enabled. This keeps development JSX semantics aligned with the framework's esbuild-based client compilation and avoids Babel rewriting multiline attribute values before hydration.

Windows CRLF support

BCP Framework 0.1.20 added source line-ending normalization for the development React Refresh path.

Application source is normalized from CRLF (\r\n) and standalone CR (\r) to LF (\n) before Babel processes development modules. This prevents Windows line endings from introducing carriage-return differences between SSR and the client bundle.

The 0.1.20 fix correctly removed carriage-return mismatches, but a separate Babel JSX whitespace normalization issue remained for multiline quoted JSX attributes. That remaining transform-parity issue is addressed by 0.1.21.

Developers do not need to rewrite multiline className values as one-line strings to work around either framework issue.

Supported multiline patterns

Both of these patterns are valid application code:

TypeScript
<div
    className="
        min-h-screen
        bg-white
        text-slate-950
    "
/>

and:

TypeScript
<div
    className={`
        min-h-screen
        bg-white
        text-slate-950
    `}
/>

BCP should hydrate them deterministically without requiring application-specific whitespace workarounds.

What BCP fixes automatically

The framework fixes deterministic source-transform differences. It does not hide genuine hydration differences caused by application behavior.

BCP applications should still avoid producing different initial values on the server and client from code such as:

TypeScript
const value = Date.now();
TypeScript
const value = Math.random();

or:

TypeScript
const browserOnly =
    typeof window !== "undefined";

when those values directly change the initial rendered markup.

Other common application-level causes include:

  • locale-dependent formatting that differs between server and browser,
  • data that changes between SSR and hydration without a serialized snapshot,
  • browser-only state initialized from localStorage, sessionStorage or matchMedia,
  • invalid HTML nesting,
  • browser extensions that modify the DOM before React hydrates it.

Recommended pattern for browser-only state

If a value genuinely depends on the browser, initialize the server/client render deterministically and update it after mount.

TypeScript
"use client";

import {
    useEffect,
    useState,
} from "react";

export default function BrowserValue() {
    const [ready, setReady] =
        useState(false);

    useEffect(
        () => {
            setReady(true);
        },
        []
    );

    return (
        <span>
            {ready
                ? "Browser ready"
                : "Loading"}
        </span>
    );
}

Development pipeline

The 0.1.21 development client transform is intentionally split by responsibility:

text
application TS/TSX
      ↓
line-ending normalization
      ↓
Babel
  - remove TypeScript syntax
  - inject React Refresh registrations
  - preserve JSX
      ↓
esbuild
  - compile JSX
  - use development JSX runtime
  - bundle application modules
      ↓
React hydration

The key rule is that React Refresh instrumentation must not change the semantic value of JSX attributes compared with SSR.

Production client compilation already uses the esbuild production pipeline and does not use the development React Refresh Babel transform.

Troubleshooting

If React still reports a hydration mismatch after upgrading to a BCP release containing the 0.1.21 fix:

  1. stop the BCP dev server,
  2. remove .bcp-framework/,
  3. start bcp dev again,
  4. hard-refresh the browser,
  5. inspect the first differing server/client value in the React hydration warning,
  6. check for request-time, random, locale, browser-only or externally changing values.

For framework diagnostics, compare the initial SSR HTML with the generated development client bundle. If the same static JSX attribute produces different strings, treat it as a framework transform-parity regression.

Do not use suppressHydrationWarning as a general fix. It should only be used when a difference is intentional and understood.

Regression coverage

The framework test suite contains a Windows-style CRLF fixture using the same multiline quoted JSX className pattern that exposed the issue in a real BCP application.

The regression test extracts the generated className value from the development bundle and verifies that its semantic string value still contains the expected line breaks and indentation instead of Babel's collapsed whitespace form.