Skip to content

Hydration

Hydration in Juice is selective, not global. The HTML page renders on the server, then only components marked with 'use client' become interactive in the browser.

How Hydration Works in Juice

Juice has two render paths that share the same component tree:

  • Initial request (HTML): server renders the full document. Browser paints immediately.
  • Client hydration: React hydrates only client boundaries ('use client' files).
  • Subsequent navigation (RSC): client fetches text/x-component payloads and swaps page segments without full reload.

Server components never hydrate in the browser. They produce HTML and stay server-only by design.

What Juice Injects at Render Time

During HTML rendering, Juice injects bootstrap modules in this order: client entry (the hydration bootstrap), client component chunks, then HMR client in development. This ordering is how the browser gets the code needed to hydrate client boundaries.

The Most Important Rule

Your interactive code must be compiled by the Juice Vite plugin. Hydration requires Juice's manifest, client chunk mapping, and bootstrap scripts. If a page is built in a separate project pipeline, Juice cannot hydrate it.

// Works: component is in Juice app and compiled by Juice plugin
'use client';
import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

Checklist: Why Hydration Is Not Working

1) Missing or misplaced 'use client'

The directive must be the first statement in the file (before imports). If it appears later, Juice treats the module as server-only and no client chunk is emitted.

2) Page is outside the Juice compile pipeline

If your page lives in another project or is rendered by a different build system, it will not be registered in Juice's client module registry. Result: HTML renders, but no interactivity.

3) You are only verifying in dev and expecting production behavior

Always verify hydration issues in a production build too. Production is the source of truth for RSC + selective hydration behavior.

bun run build
bun run preview

4) Client bundle/bootstrap not loading in browser

Open DevTools and check that module scripts and client chunks load successfully (no 404/500). A failed script request means React never hydrates.

5) Runtime serving stale manifest

Hydration depends on flight-manifest.json. If stale, the runtime can render old routes or miss new client modules. Rebuild to regenerate.

Debug Workflow (Fast)

  1. Pick one broken interactive component and confirm 'use client' is first.
  2. Confirm that file is imported by a route/layout inside app/routes/.
  3. Run production build and preview locally.
  4. In browser network tab, verify JS module scripts/chunks load without errors.
  5. Click a link and verify navigation can request Accept: text/x-component.

Compliance Rules for Juice Hydration

  • Directive correctness: 'use client' must be the first statement in the module.
  • Boundary separation: do not mix 'use client' and 'use server' in the same file.
  • Client safety: client modules must not import Node builtins like fs, path, child_process, net.
  • Runtime contract: run server and manifest from the same build output.
  • Navigation contract: for SPA transitions, client requests text/x-component and server returns RSC payloads.

Using Juice with a Separate Frontend Project

If your UI is in another project, choose one architecture and keep boundaries explicit:

  • Recommended: move interactive route components into the Juice app so Juice compiles and hydrates them.
  • Alternative: keep Juice as API/RSC backend and let the other frontend fully own hydration with its own React root.

Mixing output from one toolchain with hydration expectations from another is the most common cause of "it renders but does not hydrate".

Minimal Working Pattern

// app/routes/home.tsx (server component)
import React from 'react';
import { Counter } from '../components/counter';

export default function Home() {
  return (
    <div>
      <h1>Home</h1>
      <Counter />
    </div>
  );
}

// app/components/counter.tsx (client component)
'use client';
import React, { useState } from 'react';

export function Counter() {
  const [n, setN] = useState(0);
  return <button onClick={() => setN(n + 1)}>Clicked {n} times</button>;
}

Troubleshooting Matrix

SymptomLikely CauseFix
Button renders but click does nothingComponent not marked as client boundaryAdd 'use client' as first statement
Works in one app but not anotherDifferent build pipeline, missing Juice plugin passCompile interactive code in Juice project
Random stale behavior after deployStale flight-manifest.jsonRebuild and redeploy manifest + server together
Navigation causes full reloadClient navigation bootstrap/chunks not runningCheck script load failures and console errors