SSR & Next.js
Both @portalsdk/core and @portalsdk/react are client-only — meant to run in a browser,
not inside server code. Only @portalsdk/react's dist ships a "use client" directive
(@portalsdk/core has no React dependency and doesn't need one; its hooks are the actual
Client Component boundary).
@portalsdk/react's hooks are SSR-inert, not throwing. During server rendering
(typeof window === "undefined" — which includes a Next.js Client Component's server
prerender pass, which runs despite "use client"), useChannel/useInbox render a stable
idle snapshot instead of connecting: no acquire(), no network, nothing thrown. On an actual
client this never engages — behavior there is unchanged.
App Router
Render anything that touches useChannel or useInbox from a Client Component, same as any
other stateful hook — no special wrapper is required:
// file: chat-room.tsx
// app/chat/chat-room.tsx
"use client";
import { useChannel } from "@portalsdk/react";
interface ChatMessage {
text: string;
}
export function ChatRoom() {
const { messages, send } = useChannel<ChatMessage>({ channelId: "lobby" });
return (
<div>
<ul>
{messages.map((m) => (
<li key={m.id}>{m.content.text}</li>
))}
</ul>
<button onClick={() => send({ content: { text: "hi" } })}>Send</button>
</div>
);
}
A Server Component page can render that Client Component normally — Portal only needs to own the leaf that actually calls the hooks; everything above it in the tree can stay a Server Component:
// file: page.tsx
// app/chat/page.tsx
import { ChatRoom } from "./chat-room";
export default function ChatPage() {
return <ChatRoom />;
}
The same applies to PortalProvider — see PortalProvider for where
to put the client instance and provider in an App Router tree.
dynamic(..., { ssr: false }) is not required
That workaround exists for hooks that genuinely can't tolerate running during server
rendering. Portal's hooks can: during the server's prerender pass, ChatRoom above renders
its inert idle state (status: "idle", empty messages) rather than throwing or connecting;
once the same component re-renders in the browser, it connects normally. You only need
ssr: false here for reasons unrelated to Portal — e.g. another dependency in the same
component that itself doesn't tolerate server rendering.
Hydration
useSyncExternalStore's getServerSnapshot argument is wired to the same idle snapshot the
hook renders during SSR. Since no handle is created server-side, the client's first
(pre-hydration-effects) render produces the identical inert shape too — so there's nothing
for React to warn about mismatching between server and client output.