Server hooks

useServerRequest — the SSR-only escape hatch for reading cookies, headers, and the URL during render.

The only true "server hook" in Voltro is useServerRequest. It exposes the request snapshot during SSR so layouts + pages can read cookies, headers, and the URL during the render pass.

useServerRequest()

import { useServerRequest } from '@voltro/web'

const req = useServerRequest()
// req is { cookies, headers, url } on the server, null on the client

Returns:

interface ServerRequest {
  readonly cookies: Readonly<Record<string, string>>
  readonly headers: Readonly<Record<string, string>>
  readonly url:     string   // includes query string
}

Or null on the client (after hydration).

What it's for

The classic use case: decode the session cookie during SSR so the layout renders the right subject-aware UI:

// src/pages/layout.tsx
import { useServerRequest } from '@voltro/web'
import { SubjectProvider } from '@voltro/plugin-auth/web'
import { decodeSubjectFromRequest } from './lib/auth'

export default function Layout({ children }) {
  const req = useServerRequest()
  const subject = decodeSubjectFromRequest(req)
  return <SubjectProvider subject={subject}>{children}</SubjectProvider>
}

decodeSubjectFromRequest is app code (it knows the cookie name + how to verify the signature). The hook just hands you the cookies.

Reading query params

For SSR pages that need to parse the URL's query string:

import { useSearchParams } from '@voltro/web'

export const renderMode = 'ssr' as const

export default function SearchPage() {
  // Native `URLSearchParams`, resolved server-side from the SSR request
  // URL (correct in the first paint) and from `window.location.search`
  // on the client — same call site on both sides.
  const q = useSearchParams().get('q') ?? ''
  const results = q ? searchDocs(q) : []
  return /* … */
}

useSearchParams() returns the request's query as a native URLSearchParams. SPA pages that must react to router-pushed query changes without a reload re-render through the router (useNavigate/useLocation); the hook then re-resolves on that render.

Reading cookies

const req = useServerRequest()
const sessionCookie = req?.cookies['voltro:session']

cookies is already parsed — no manual Cookie: header splitting needed.

Reading headers

const req = useServerRequest()
const userAgent = req?.headers['user-agent']
const tenant = req?.headers['x-tenant']

Headers are lower-cased keys. Multi-value headers are joined with , .

Conditional rendering by SSR vs client

const req = useServerRequest()
if (req) {
  // SSR — req available
} else {
  // Post-hydration — read browser-side equivalents
  const ua = navigator.userAgent
}

For values that need to flow from SSR to client (cookie-derived subject, locale), serialise them via SubjectProvider-style context. The client doesn't re-read cookies — it inherits what the SSR pass decoded.

What it's NOT for

  • Reading database state. Use a loader — it runs server-side too, but with a typed params + abort signal + a clear server/client divide.
  • Triggering side effects during render. React's render must be pure. For SSR side effects (logging a request), use middleware in app.config.ts.runtime.
  • Client-side cookie reading. The session cookie is HttpOnly — JS can't read it. The server decoded it during SSR + handed you the subject; that's what the client sees.

Mounting check pattern

const req = useServerRequest()
const isSSR = req !== null

return (
  <div>
    {isSSR ? <ServerOnlyBranch /> : <ClientOnlyBranch />}
  </div>
)

Use sparingly — divergent SSR / client rendering causes hydration mismatches. The cleaner pattern is useEffect(() => setHydrated(true), []) + render the same JSX in both paths.

See also