npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

rsc-state

v1.0.0

Published

Type-safe state management for React Server Components

Readme

rsc-state

Type-safe state management for React Server Components.

Features

  • Two storage modes: Request-scoped (isolated per user) or persistent (shared across requests)
  • Type-safe: Full TypeScript inference with derived state
  • Zero client JS: Purely server-side using React's cache API
  • No hooks needed: Direct read/write API for Server Components
  • Derived state: Compute values automatically from base state (memoized for performance)
  • Middleware: Intercept and transform state operations with async support
  • Storage adapters: Pluggable backends for persistent mode (Redis, database, etc.)
  • Async callbacks: onInitialize, onUpdate, onReset support async operations
  • Batch updates: Combine multiple updates for better performance

Installation

npm install rsc-state

Migration from v1.x to v2.x

Version 2.0 introduces an async-first architecture. All state-mutating methods now return promises.

Breaking Changes

| Method | v1.x | v2.0 | | -------------- | ------ | ---------------- | | initialize() | void | Promise<void> | | update() | void | Promise<void> | | set() | void | Promise<void> | | patch() | N/A | Promise<void> | | reset() | void | Promise<void> | | batch() | void | Promise<void> | | read() | sync | sync (unchanged) | | select() | sync | sync (unchanged) |

Migration Steps

  1. Add await before all state-mutating calls:
// Before (v1.x)
userStore.initialize({ userId: "123", userName: "John" });
userStore.update((state) => ({ ...state, count: state.count + 1 }));
userStore.set({ userId: null, userName: "" });
userStore.reset();

// After (v2.0)
await userStore.initialize({ userId: "123", userName: "John" });
await userStore.update((state) => ({ ...state, count: state.count + 1 }));
await userStore.set({ userId: null, userName: "" });
await userStore.reset();
  1. Ensure your Server Components and layouts are async:
// Before (v1.x)
export default function RootLayout({ children }) {
  userStore.initialize({ userId: "123" });

  return <html><body>{children}</body></html>;
}

// After (v2.0)
export default async function RootLayout({ children }) {
  await userStore.initialize({ userId: "123" });

  return <html><body>{children}</body></html>;
}

Storage Modes

Request Storage (Default)

State is isolated per request using React's cache API. Safe for user-specific data.

const userStore = createServerStore({
	storage: "request", // default, can be omitted
	initial: { userId: null, userName: "" },
});

Use for: User sessions, authentication, user preferences, any user-specific data.

How it works:

  1. Layout reads from data source (cookies/database)
  2. Layout initializes store with that data
  3. Any component reads from store (no props needed!)
  4. Server Action updates data source → revalidates
  5. New request: layout re-initializes store

Persistent Storage

State persists across requests using module-level storage. Shared across ALL users.

const settingsStore = createServerStore({
	storage: "persistent",
	initial: { theme: "light" },
});

Use for: Feature flags, app-wide configuration, demos/prototypes.

Warning: Not suitable for user-specific data. State is lost on server restart and not synced across server instances.

Quick Start

Request-Scoped Store (User Data)

// lib/stores.ts
import { createServerStore } from "rsc-state";

export const userStore = createServerStore({
	initial: {
		userId: null as string | null,
		userName: "",
	},
	derive: (state) => ({
		isAuthenticated: state.userId !== null,
	}),
});

// app/layout.tsx - Initialize from cookies each request
import { cookies } from "next/headers";
import { userStore } from "@/lib/stores";

export default async function Layout({ children }) {
	const cookieStore = await cookies();
	const userId = cookieStore.get("userId")?.value ?? null;
	const userName = cookieStore.get("userName")?.value ?? "";

	await userStore.initialize({ userId, userName });

	return <html><body>{children}</body></html>;
}

// components/Header.tsx - Read without props
import { userStore } from "@/lib/stores";

export function Header() {
	const user = userStore.read(); // No props needed!

	return <h1>Welcome, {user.isAuthenticated ? user.userName : "Guest"}</h1>;
}

// lib/actions.ts - Update cookies, revalidate
"use server";
import { cookies } from "next/headers";
import { revalidatePath } from "next/cache";

export async function login(formData: FormData) {
	const cookieStore = await cookies();

	cookieStore.set("userId", "123");
	cookieStore.set("userName", formData.get("name") as string);

	revalidatePath("/"); // New request → layout re-initializes store
}

Persistent Store (Global App State)

// lib/stores.ts
import { createServerStore } from "rsc-state";

export const settingsStore = createServerStore({
	storage: "persistent",
	initial: { theme: "light" as "light" | "dark" },
	derive: (state) => ({
		isDarkMode: state.theme === "dark",
	}),
});

// app/layout.tsx - Just read, no initialization needed
import { settingsStore } from "@/lib/stores";

export default function Layout({ children }) {
	const settings = settingsStore.read();

	return (
		<html>
			<body style={{ background: settings.isDarkMode ? "#121212" : "#fff" }}>
				{children}
			</body>
		</html>
	);
}

// lib/actions.ts - Update store directly, revalidate
"use server";
import { revalidatePath } from "next/cache";
import { settingsStore } from "./stores";

export async function toggleTheme() {
	const current = settingsStore.read().theme;

	await settingsStore.set({ theme: current === "light" ? "dark" : "light" });

	revalidatePath("/");
}

API

createServerStore(configuration)

Creates a new server store.

Parameters:

| Option | Type | Description | | -------------- | ------------------------------------------------------------------- | ----------------------------------------------------- | | initial | T \| () => T | Initial state value or factory function | | storage | "request" \| "persistent" | Storage mode (default: "request") | | derive | (state: T) => D | Optional function to compute derived state (memoized) | | middleware | Middleware<T>[] | Array of middleware functions to intercept operations | | adapter | StorageAdapter<T> | Storage adapter for persistent mode | | debug | boolean | Enable debug logging | | onInitialize | (state: T) => void \| Promise<void> | Callback after store initialization | | onUpdate | (prev: T, next: T) => void \| Promise<void> | Callback after state updates | | onReset | () => void \| Promise<void> | Callback after store reset | | onError | (error: Error, context: ErrorContext<T>) => void \| Promise<void> | Callback when derive function throws |

Returns: ServerStore<T, D> instance

Store Methods

| Method | Description | | ------------------- | ------------------------------------------------------------ | | initialize(state) | Initialize store with values (async, for request-scoped) | | read() | Read current state including derived properties (sync) | | update(fn) | Update state via reducer: (prev) => next (async) | | set(state) | Replace entire state (async) | | patch(partial) | Partially update state by merging (async) | | select(fn) | Select specific value: (state) => value (sync) | | reset() | Reset to initial state (async) | | batch(fn) | Execute multiple updates, compute derived state once (async) |

Middleware

Intercept and transform state operations:

import type { Middleware } from "rsc-state";

const loggingMiddleware: Middleware<MyState> = (operation) => {
	console.log(`[${operation.type}]`, operation.previousState, "→", operation.nextState);
	return operation.nextState;
};

const validationMiddleware: Middleware<MyState> = async (operation) => {
	await validateState(operation.nextState);
	return operation.nextState;
};

const store = createServerStore({
	initial: { count: 0 },
	middleware: [loggingMiddleware, validationMiddleware],
});

Middleware receives:

  • operation.type: "initialize" | "update" | "set" | "patch" | "reset" | "batch"
  • operation.previousState: State before the operation
  • operation.nextState: State after the operation (can be transformed)

Middleware can:

  • Log state changes
  • Validate state
  • Transform state before it's applied
  • Perform async operations

Storage Adapters

Use custom storage backends for persistent mode:

import type { StorageAdapter } from "rsc-state";

const redisAdapter: StorageAdapter<MyState> = {
	read: async () => {
		const data = await redis.get("store:key");
		return data ? JSON.parse(data) : null;
	},
	write: async (state) => {
		await redis.set("store:key", JSON.stringify(state));
	},
};

const store = createServerStore({
	storage: "persistent",
	initial: { theme: "light" },
	adapter: redisAdapter,
});

Patch Method

Partially update state without spread syntax:

// Instead of:
await store.update((state) => ({ ...state, userName: "John" }));

// Use:
await store.patch({ userName: "John" });

// Update multiple properties:
await store.patch({ userName: "John", email: "[email protected]" });

Derived State Memoization

Derived state is automatically memoized and only recalculated when the base state changes:

const store = createServerStore({
	initial: { items: [] as number[] },
	derive: (state) => ({
		// Only recalculated when `items` changes
		total: state.items.reduce((sum, item) => sum + item, 0),
		count: state.items.length,
	}),
});

store.read(); // Computes derived state
store.read(); // Returns cached result (no recomputation)
await store.update((s) => ({ items: [...s.items, 5] }));
store.read(); // Recomputes derived state

Lifecycle Hooks

React to state changes with lifecycle callbacks (supports async):

const store = createServerStore({
	initial: { count: 0 },
	onInitialize: async (state) => {
		console.log("Store initialized with:", state);
		await analytics.track("store_initialized", state);
	},
	onUpdate: async (previousState, nextState) => {
		console.log("State changed from", previousState, "to", nextState);
		await syncToDatabase(nextState);
	},
	onReset: async () => {
		console.log("Store was reset");
		await clearCache();
	},
});

Error Handling

Handle errors in derive functions gracefully:

const store = createServerStore({
	initial: { data: null as Data | null },
	derive: (state) => ({
		// This might throw if data is null
		processedData: processData(state.data!),
	}),
	onError: async (error, context) => {
		console.error(`Error in ${context.method}:`, error.message);
		await reportError(error);
	},
});

// When derive throws, base state is returned without derived properties
const state = store.read(); // { data: null } - no crash!

Batch Updates

Combine multiple updates for better performance:

const store = createServerStore({
	initial: { count: 0, name: "" },
	derive: (state) => ({
		description: `${state.name} has count ${state.count}`,
	}),
});

// Without batch: derived state recalculated 3 times
await store.update((s) => ({ ...s, count: s.count + 1 }));
await store.update((s) => ({ ...s, count: s.count + 1 }));
await store.set({ count: 100, name: "Final" });

// With batch: derived state recalculated only once
await store.batch((api) => {
	api.update((s) => ({ ...s, count: s.count + 1 }));
	api.update((s) => ({ ...s, count: s.count + 1 }));
	api.set({ count: 100, name: "Final" });
});

// Batch also supports patch:
await store.batch((api) => {
	api.patch({ count: 10 });
	api.patch({ name: "Updated" });
});

Examples

Working examples are available for different Next.js versions:

| Version | Directory | Notes | | --------------------- | ---------------------------------------------------------- | ---------------------------------------- | | Next.js 14 | examples/next-14 | Synchronous cookies() API | | Next.js 15 | examples/next-15 | Async cookies() API, Turbopack | | Next.js 16 | examples/next-16 | Async cookies() API, Turbopack default | | Next.js 16 + Adapters | examples/next-16-adapters | Storage adapters with file persistence |

cd examples/next-15  # or next-14, next-16, next-16-adapters
npm install
npm run dev

Each example demonstrates:

  • Both storage modes side by side
  • Request-scoped user store with cookie persistence
  • Persistent theme store without cookies
  • Reading state without prop drilling
  • Server Actions with revalidation

The adapters example additionally shows:

  • Custom file-based storage adapter (JSON persistence)
  • In-memory adapter for comparison
  • State that survives server restarts
  • Middleware for logging operations

Framework Support

| Framework | RSC Status | Support | | -------------- | -------------- | --------------- | | Next.js 14+ | Stable | ✅ Full support | | React Router 7 | Preview | ⏳ Coming soon | | TanStack Start | In development | ⏳ Coming soon | | Waku | Stable | ⏳ Planned |

How It Works

Request Storage

Uses React's cache API for request-scoped memoization:

  • Cache is automatically invalidated between server requests
  • Same cached instance shared across all components in one request
  • No state leakage between different users/requests

Persistent Storage

Uses module-level variables:

  • State persists in Node.js process memory
  • Shared across all requests and users
  • Lost on server restart or deployment
  • Can use storage adapters for external persistence (Redis, database, etc.)

TypeScript

Full type inference for state and derived values:

const store = createServerStore({
	initial: {
		userId: null as string | null,
		userName: "",
	},
	derive: (state) => ({
		isAuthenticated: state.userId !== null,
	}),
});

// Type: { userId: string | null; userName: string; isAuthenticated: boolean }
const state = store.read();

// Type error: Property 'invalidProperty' does not exist
state.invalidProperty;

// patch() is type-safe
await store.patch({ userName: "John" }); // ✅
await store.patch({ invalidKey: "value" }); // ❌ Type error

Contributing

See CONTRIBUTING.md

License

MIT