DEV Community

Nainik Mehta
Nainik Mehta

Posted on

Next.js App Router — WebSockets via Client Islands

The Challenge: Realtime in the Age of Server Components

The paradigm shift toward React Server Components (RSC) and the Next.js App Router has fundamentally changed how we architect web applications. We are now defaulting to server-side rendering, which is fantastic for performance, SEO, and initial load times. However, a common friction point arises when we need to inject high-frequency, bidirectional realtime data into these server-rendered pages.

Too often, developers fall into the trap of importing heavy socket libraries directly into their server components or wrapping their entire application in massive context providers, effectively bloating the client bundle and negating the performance gains of the App Router.

The Solution: The "Client Island" Pattern

Instead of fighting the architecture, we can embrace "Client Islands"—a pattern where we isolate the stateful, client-side logic into a tiny, focused leaf component. By keeping the WebSocket management strictly client-side, we ensure that our server-rendered pages remain lightweight, fast, and cacheable.

Implementing the WebSocket Island

The goal is to keep the WebSocket connection lifecycle outside of the rendering flow. We utilize useEffect to manage the connection, ensuring it only runs on the client, and we tap into data fetching libraries like TanStack Query or SWR to surgically update the UI.

'use client';

import { useEffect } from 'react';
import { useQueryClient } from '@tanstack/react-query';

export function RealtimeSync({ token }) {
  const queryClient = useQueryClient();

  useEffect(() => {
    const ws = new WebSocket(`wss://realtime.example.com?token=${token}`);

    ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      queryClient.setQueryData(['items'], data);
    };

    return () => ws.close();
  }, [token, queryClient]);

  return null; // This component renders nothing, just manages the side effect
}
Enter fullscreen mode Exit fullscreen mode

Persistence via RootLayout

To prevent the connection from dropping and reconnecting every time a user navigates between routes, you should lift the connection logic into a RootLayout provider. By wrapping your application in a persistent provider, the WebSocket instance survives client-side navigation, providing a seamless user experience.

Navigating Deployment Realities

Deployment choices are critical when dealing with WebSockets in a Next.js environment. Classic serverless platforms often struggle with persistent connections.

  1. Managed Realtime Providers: Services like Ably, Pusher, or PartyKit are often the most reliable choice. They abstract away the complexity of connection scaling and pub/sub distribution.
  2. Self-Hosted Servers: If you prefer control, a dedicated Node.js server using ws or Socket.IO works well, but you must handle cross-instance synchronization.
  3. Experimental Upgrades: Features like Vercel’s experimental_upgradeWebSocket offer a path forward, but remember that these are pinned to specific function instances. You will need a Redis pub/sub layer to ensure your events broadcast correctly across all active instances.

Auth, Reconnection, and Reliability

Realtime is only as good as its reliability. When implementing this pattern:

  • Ephemeral Tokens: Never pass long-lived credentials. Mint a short-lived token server-side and pass it to your island.
  • Backoff Strategies: Always implement exponential backoff with jitter to prevent "thundering herd" problems during network instability.
  • Rehydration: Upon reconnection, always trigger a re-fetch or re-validation of your data cache to ensure the client state matches the server source of truth.

When to Choose SSE Over WebSockets

Not every realtime feature requires a WebSocket. If your data flow is strictly one-way (server-to-client) and low-frequency, Server-Sent Events (SSE) are significantly easier to implement. SSE is CDN-friendly and integrates much more naturally with standard HTTP streaming in Next.js. Reserve WebSockets for truly bidirectional, low-latency interactions where the overhead is justified.

Conclusion

The "Client Island" pattern is a pragmatic way to leverage the power of the App Router while maintaining the dynamic, interactive features our users expect. By isolating WebSocket logic, we keep our bundles lean and our server components clean.

What is your preferred approach for handling realtime in your Next.js projects? Are you sticking with managed providers, or are you building out custom infrastructure? Let’s discuss in the comments.

Top comments (0)