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-componentpayloads 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 preview4) 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)
- Pick one broken interactive component and confirm
'use client'is first. - Confirm that file is imported by a route/layout inside
app/routes/. - Run production build and preview locally.
- In browser network tab, verify JS module scripts/chunks load without errors.
- 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-componentand 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
| Symptom | Likely Cause | Fix |
|---|---|---|
| Button renders but click does nothing | Component not marked as client boundary | Add 'use client' as first statement |
| Works in one app but not another | Different build pipeline, missing Juice plugin pass | Compile interactive code in Juice project |
| Random stale behavior after deploy | Stale flight-manifest.json | Rebuild and redeploy manifest + server together |
| Navigation causes full reload | Client navigation bootstrap/chunks not running | Check script load failures and console errors |