The Dinou Pattern
Combine Server Functions, Headless Client Updaters, and React 19 primitives (use() and <Suspense>) with your global store (such as Zustand) for surgical, zero-layout-shift reactivity — without route-wide reloads or lost input focus.
The Concept & Architecture
In full-stack React applications, mutations often trigger broad page re-renders or full route re-fetches. This can cause unnecessary re-renders of unrelated components, reset unsaved form inputs, or cause visible layout shifts.
The Dinou Pattern offers a modern alternative by leveraging the unique ability of Server Functions to return Client Components directly over the React Flight RPC protocol. After executing a mutation securely in Node.js, the server returns an invisible Headless State Updater component that:
- Mounts on the client (rendering
null, emitting 0 DOM nodes and 0 layout shifts). - Executes a client-side
useEffectupon mounting to notify a store. - Selectively invalidates only dependent queries, streaming the updated UI via React 19
<Suspense>anduse()without full-page reloads.
Flight RPC & Headless Updaters
Because Server Functions communicate using the React Flight protocol rather than plain JSON REST endpoints, the server can return serialized references to Client Components with custom props:
The Server Function owns the business logic, validates inputs, and updates the database in secure Node.js runtime.
Returning the headless component allows the server to orchestrate client state transitions without coupling to client network listeners.
5-Step Execution Lifecycle
The following flow illustrates how the mutation, stream, store update, and surgical re-render operate in unison:
addPatternTask()) and sets a mutation promise into a headless boundary.<PatternTaskUpdater /> Client Component.null), and its useEffect invalidates the key in the global store (e.g. useTaskStore.getState().invalidateTasks()).<Suspense> boundary re-fetches and streams the updated list, leaving form focus and scroll untouched.Step-by-Step Implementation
Here is the complete, modular implementation showing how Server Functions, Flight RPC, and Zustand orchestrate surgical reactivity without route revalidations:
1. The Global Store (Zustand)
A minimal store holding a reactive invalidation key. Any global state manager (such as Zustand, Jotai, or Redux) works identically:
// src/stores/task-store.ts
import { create } from "zustand";
interface TaskStore {
tasksKey: number;
invalidateTasks: () => void;
}
export const useTaskStore = create<TaskStore>((set) => ({
tasksKey: 0,
invalidateTasks: () => set((state) => ({ tasksKey: state.tasksKey + 1 })),
}));Why it matters: Calling invalidateTasks() increments tasksKey, signaling subscribing components to re-fetch their Suspense stream without triggering route-wide re-renders.
2. The Headless Client Updater
An invisible Client Component that renders null (0 DOM elements), but invokes the store's invalidation action when mounted:
// src/components/pattern-updater.tsx
"use client";
import { useEffect } from "react";
import { useTaskStore } from "../stores/task-store";
export default function PatternTaskUpdater({ id }: { id?: string }) {
useEffect(() => {
// Atomically signal the store outside the render cycle
useTaskStore.getState().invalidateTasks();
}, [id]);
return null; // Headless component: emits zero DOM nodes and 0 layout shifts
}3. The Server Function Mutation
Performs the backend mutation in Node.js and returns the Headless Updater component over Flight RPC:
// src/server-functions/pattern-demo.tsx
"use server";
import type { ReactNode } from "react";
import PatternTaskUpdater from "../components/pattern-updater";
import { tasksDb } from "./db";
export async function addPatternTask(text: string): Promise<ReactNode> {
const trimmed = text.trim();
const id = Math.random().toString(36).substring(2, 9);
if (trimmed) {
tasksDb.unshift({
id,
text: trimmed,
createdAt: new Date().toLocaleTimeString(),
});
}
// The server orchestrates the client by returning the Headless Updater!
return <PatternTaskUpdater key={id} id={id} />;
}4. Server Data Query Function
Fetches and renders the updated list directly on the server:
// src/server-functions/pattern-demo.tsx (continued)
"use server";
import type { ReactNode } from "react";
import PatternTasksList from "../components/pattern-tasks-list";
import { tasksDb } from "./db";
export async function fetchPatternTasks(): Promise<ReactNode> {
return <PatternTasksList tasks={[...tasksDb]} />;
}5. Display Component (Client Component)
Renders the list of tasks received from the server query:
// src/components/pattern-tasks-list.tsx
"use client";
export interface PatternTaskItem {
id: string;
text: string;
createdAt: string;
}
export default function PatternTasksList({ tasks }: { tasks: PatternTaskItem[] }) {
if (!tasks || tasks.length === 0) {
return <p className="text-muted-foreground text-sm">No tasks yet.</p>;
}
return (
<ul className="space-y-2">
{tasks.map((task) => (
<li key={task.id} className="p-2.5 rounded-lg border bg-card flex justify-between">
<span>{task.text}</span>
<span className="text-xs text-muted-foreground">{task.createdAt}</span>
</li>
))}
</ul>
);
}6. Putting It All Together in the Page
The view coordinates mutation promises and queries with React 19's native use() hook and <Suspense>. Switch tabs below to explore each component in the unified architecture:
"use client";
import { useState, useEffect, Suspense, use, type ReactNode } from "react";
import { addPatternTask, fetchPatternTasks } from "./server-functions/pattern-demo";
import { useTaskStore } from "./stores/task-store";
function TasksStream({ promise }: { promise: Promise<ReactNode> }) {
// Native React 19: unroll the Flight stream promise
const content = use(promise);
return <>{content}</>;
}
function MutationStream({ promise }: { promise: Promise<ReactNode> }) {
const content = use(promise);
return <>{content}</>;
}
export default function Page() {
const tasksKey = useTaskStore((state) => state.tasksKey);
const [text, setText] = useState("");
const [isMutating, setIsMutating] = useState(false);
const [mutationKey, setMutationKey] = useState(0);
const [mutationPromise, setMutationPromise] = useState<Promise<ReactNode> | null>(null);
const [tasksPromise, setTasksPromise] = useState<Promise<ReactNode>>(() => fetchPatternTasks());
// Invalidate and re-fetch only when the store increments tasksKey
useEffect(() => {
if (tasksKey > 0) {
setTasksPromise(fetchPatternTasks());
}
}, [tasksKey]);
const handleAddTask = (e: React.FormEvent) => {
e.preventDefault();
const trimmed = text.trim();
if (!trimmed || isMutating) return;
setIsMutating(true);
setMutationKey((k) => k + 1);
const p = addPatternTask(trimmed);
p.finally(() => {
setIsMutating(false);
setText("");
});
setMutationPromise(p);
};
return (
<div className="space-y-6">
{/* 1. Mutation Form */}
<form onSubmit={handleAddTask} className="flex gap-2">
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="New task..."
disabled={isMutating}
className="input"
/>
<button type="submit" disabled={isMutating || !text.trim()} className="btn-primary">
{isMutating ? "Adding..." : "Add Task"}
</button>
</form>
{/* 2. Headless Mutation Mount (renders null, updates store) */}
{mutationPromise && (
<Suspense key={`mutation-${mutationKey}`} fallback={null}>
<MutationStream promise={mutationPromise} />
</Suspense>
)}
{/* 3. Surgical Reactive Tasks List */}
<Suspense
key={`pattern-tasks-${tasksKey}`}
fallback={<div>Streaming updated list from Node.js...</div>}
>
<TasksStream promise={tasksPromise} />
</Suspense>
</div>
);
}import { create } from "zustand";
interface TaskStore {
tasksKey: number;
invalidateTasks: () => void;
}
export const useTaskStore = create<TaskStore>((set) => ({
tasksKey: 0,
invalidateTasks: () => set((state) => ({ tasksKey: state.tasksKey + 1 })),
}));"use client";
import { useEffect } from "react";
import { useTaskStore } from "../stores/task-store";
export default function PatternTaskUpdater({ id }: { id?: string }) {
useEffect(() => {
// Atomically signal the store outside the render cycle
useTaskStore.getState().invalidateTasks();
}, [id]);
return null; // Headless component: emits zero DOM nodes and 0 layout shifts
}"use server";
import type { ReactNode } from "react";
import PatternTaskUpdater from "../components/pattern-updater";
import PatternTasksList from "../components/pattern-tasks-list";
import { tasksDb } from "./db";
export async function addPatternTask(text: string): Promise<ReactNode> {
const trimmed = text.trim();
const id = Math.random().toString(36).substring(2, 9);
if (trimmed) {
tasksDb.unshift({
id,
text: trimmed,
createdAt: new Date().toLocaleTimeString(),
});
}
// The server orchestrates the client by returning the Headless Updater!
return <PatternTaskUpdater key={id} id={id} />;
}
export async function fetchPatternTasks(): Promise<ReactNode> {
return <PatternTasksList tasks={[...tasksDb]} />;
}"use client";
export interface PatternTaskItem {
id: string;
text: string;
createdAt: string;
}
export default function PatternTasksList({ tasks }: { tasks: PatternTaskItem[] }) {
if (!tasks || tasks.length === 0) {
return <p className="text-muted-foreground text-sm">No tasks yet.</p>;
}
return (
<ul className="space-y-2">
{tasks.map((task) => (
<li key={task.id} className="p-2.5 rounded-lg border bg-card flex justify-between">
<span>{task.text}</span>
<span className="text-xs text-muted-foreground">{task.createdAt}</span>
</li>
))}
</ul>
);
}How It Works
- User submits the form, initiating
addPatternTask()without full-page reloads. - The mutation promise is passed to a headless
<Suspense>boundary. - Server mutates the database in Node.js and returns
<PatternTaskUpdater />over Flight RPC. PatternTaskUpdatermounts, executing itsuseEffectto calluseTaskStore.getState().invalidateTasks().- The store notifies the page, calling
fetchPatternTasks()to obtain the fresh stream. - Only the tasks list
<Suspense>boundary updates smoothly, keeping inputs, state, and scroll position completely intact.
Flight RPC streams server errors as rejected promises. If the Server Function throws in Node.js, the promise rejects on the client and the Headless Updater never mounts. This guarantees that your client-side store is never corrupted with failed mutation state.
try {
const p = addPatternTask(trimmed);
setMutationPromise(p);
await p; // If Node.js throws, execution jumps to catch
} catch (error) {
// Headless updater never mounted; store was NOT modified!
console.error("Mutation failed on server:", error);
toast.error("Failed to add task");
}When triggering multiple rapid mutations, incrementing setMutationKey((k) => k + 1) on the <Suspense key=...> boundary forces React to cleanly unmount the previous updater and mount the new one fresh.
Keep Headless Updaters purely functional with zero DOM output (return null). They exist solely as the reactive bridge between the Server Function RPC response and the client-side store.
Zero-Dependency Option: Native useSyncExternalStore
If you prefer not to include any third-party state libraries in your client bundle, you can achieve the exact same surgical reactivity using React 19's native useSyncExternalStore hook:
// src/stores/native-store.ts
import { useSyncExternalStore } from "react";
let tasksKey = 0;
const listeners = new Set<() => void>();
export const nativeTaskStore = {
getSnapshot: () => tasksKey,
invalidateTasks: () => {
tasksKey += 1;
listeners.forEach((listener) => listener());
},
subscribe: (listener: () => void) => {
listeners.add(listener);
return () => listeners.delete(listener);
},
};
export function useNativeTasksKey(): number {
return useSyncExternalStore(
nativeTaskStore.subscribe,
nativeTaskStore.getSnapshot,
nativeTaskStore.getSnapshot // Server snapshot for SSR hydration safety
);
}In your Headless Updater, simply invoke nativeTaskStore.invalidateTasks() instead of useTaskStore.getState().invalidateTasks(). The lifecycle, Flight RPC streaming, and selective re-fetching behavior remain 100% identical.
Comparison & Key Advantages
Comparing the Dinou Pattern against traditional client-side data fetching (REST / React Query):
| Feature | The Dinou Pattern | REST / React Query (SPA) |
|---|---|---|
| Reactivity Scope | Surgical (targeted boundary only) | Surgical (query key invalidation) |
| Form / Input Focus | 100% Preserved (zero blur or reset) | Preserved |
| Bundle Overhead | ~1 KB (Zustand) or 0 KB (Native) | ~12–15 KB (client query library) |
| Backend Architecture | Direct Node.js Server Functions ("use server") | Requires dedicated HTTP API endpoints (/api/...) |
| Data Transport | React Flight RPC (typed component streaming) | JSON serialization + manual client fetch logic |
| Boilerplate | Zero API routes, controllers, or fetch hooks | Manual endpoints, fetch hooks, error states, and types |
| DOM Impact | 0 DOM nodes emitted by headless updater | N/A (data only) |
useSyncExternalStore alongside React 19's Flight RPC and <Suspense>.