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 🙏

© 2026 – Pkg Stats / Ryan Hefner

relay-state

v0.2.0

Published

Framework-agnostic shared state store for micro frontends, powered by window events and an in-memory cache.

Readme

relay-state

Shared state for micro frontends, designed for React. Client-side only.

CI npm license

The Problem

In micro frontend architectures like single-spa, independently deployed sub-applications share a single browser window but have no built-in way to share state. When App A updates a user's profile, App B has no idea it happened.

Common workarounds -- module federation, custom event buses cobbled together per team, or dumping state into localStorage -- are either heavyweight, fragile, or require framework coupling.

relay-state solves this with a minimal approach: an in-memory cache backed by window CustomEvent dispatch. Any micro frontend on the page can read, write, and subscribe to shared state. The library is designed for React -- the subscription API plugs directly into useSyncExternalStore and a first-class useRelayState hook is included. The core event mechanism is framework-agnostic in principle, but React is the primary target and the only officially supported integration.

Install

# pnpm
pnpm add relay-state

# npm
npm install relay-state

# Vite+
vp add relay-state

Quick Start

import { useRelayState, useRelayStateValue, useSetRelayState } from "relay-state/react";

// Tuple API — like useState, but shared across micro frontends
function Counter() {
  const [count, setCount] = useRelayState<number>("count", 0);
  return <button onClick={() => setCount((n) => (n ?? 0) + 1)}>Count: {count}</button>;
}

// Read-only — re-renders on changes, no setter
function UserBadge() {
  const user = useRelayStateValue<{ name: string; role: string }>("user");
  if (!user) return null;
  return (
    <span>
      {user.name} ({user.role})
    </span>
  );
}

// Write-only — does NOT re-render when state changes
function PromoteButton() {
  const setUser = useSetRelayState<{ name: string; role: string }>("user");
  return <button onClick={() => setUser((u) => ({ ...u, role: "admin" }))}>Promote</button>;
}

API

get<T>(key: string): T | undefined

Returns the current value for a key, or undefined if the key has not been set.

const count = get<number>("count"); // number | undefined

has(key: string): boolean

Returns true if the key exists in the store, even if its value is undefined. Useful for distinguishing between "key was set to undefined" and "key was never set."

set("key", undefined);
has("key"); // true
has("other"); // false

set<T>(key: string, value: T | ((prev: T | undefined) => T)): void

Sets a value in the store and dispatches a CustomEvent on window to notify all subscribers. Accepts either a direct value or an updater function.

// Direct value
set("count", 0);

// Updater function (receives the previous value)
set<number>("count", (prev) => (prev ?? 0) + 1);

Note: Because updater functions are detected via typeof value === "function", storing a function as a value requires wrapping it: set("callback", () => myFunction). This is the same tradeoff React's useState makes.

subscribe<T>(key: string, callback: (value: T | undefined) => void): () => void

Subscribes to changes for a specific key. The callback fires whenever set or del is called for that key, including updates originating from other micro frontend bundles. Returns an unsubscribe function.

const unsubscribe = subscribe<number>("count", (value) => {
  console.log("Count is now:", value);
});

// Stop listening
unsubscribe();

This signature is designed to work directly with React's useSyncExternalStore.

del(key: string): void

Deletes a key from the store and notifies subscribers with undefined.

del("count");

clear(): void

Removes all keys from the store and notifies all active subscribers with undefined. Use this for logout flows or full application resets.

clear();

createStore(namespace: string): RelayStore

Creates a namespaced store. All keys are internally prefixed with namespace:, preventing collisions between micro frontends while sharing the same underlying cache and event bus.

import { createStore } from "relay-state";

// In App A
const appA = createStore("appA");
appA.set("user", { name: "Leo" });
appA.get("user"); // { name: "Leo" }

// In App B
const appB = createStore("appB");
appB.set("user", { name: "Maria" });
appB.get("user"); // { name: "Maria" }

// No collision -- these are stored as "appA:user" and "appB:user"

A namespaced store returns an object with get, has, set, subscribe, del, and clear -- the same API as the global functions, scoped to the namespace.

RelayStore (type)

The interface returned by createStore:

interface RelayStore {
  get: <T = unknown>(key: string) => T | undefined;
  has: (key: string) => boolean;
  set: <T = unknown>(key: string, value: T | ((prev: T | undefined) => T)) => void;
  subscribe: <T = unknown>(key: string, callback: (value: T | undefined) => void) => () => void;
  del: (key: string) => void;
  clear: () => void;
}

React Integration

A React hook is available via the relay-state/react entrypoint. It uses useSyncExternalStore under the hood, so your components re-render automatically when shared state changes.

React 18+ is a peer dependency.

Note: relay-state is client-side only. It requires window at runtime and is not designed for server-side rendering.

useRelayState<T>(key, initialValue?) → [value, setter]

The primary hook. Returns a tuple of the current value and a setter — the same pattern as React's useState.

import { useRelayState } from "relay-state/react";

function Counter() {
  const [count, setCount] = useRelayState<number>("count", 0);
  return <button onClick={() => setCount((prev) => (prev ?? 0) + 1)}>Count: {count}</button>;
}

The setter accepts either a direct value or an updater function:

setCount(10);
setCount((prev) => (prev ?? 0) + 1);

When initialValue is provided and the key is currently unset, the value is written to the store on first mount so all consumers see the same default — regardless of which micro frontend mounts first.

useRelayStateValue<T>(key, initialValue?) → value

Subscribes to a key and returns only the current value. Use this when a component needs to read state but never write it.

import { useRelayStateValue } from "relay-state/react";

function UserBadge() {
  const user = useRelayStateValue<{ name: string }>("user");
  if (!user) return null;
  return <span>{user.name}</span>;
}

useSetRelayState<T>(key) → setter

Returns a stable setter function without subscribing to state changes. Components using only this hook will not re-render when the value changes — the key performance primitive for write-only components.

import { useSetRelayState } from "relay-state/react";

function PromoteButton() {
  const setUser = useSetRelayState<{ name: string; role: string }>("user");
  return (
    <button onClick={() => setUser((prev) => ({ ...prev, role: "admin" }))}>
      Promote to Admin
    </button>
  );
}

The relay-state/react entrypoint also re-exports all core functions (get, has, set, del, subscribe, createStore, clear) and the RelayStore type for convenience.

Best Practices

Centralize keys as constants

String keys are the contract between micro frontends. A typo in one app silently breaks the connection with another. Keep all shared keys in a single file, published as a shared package or committed to a common location all apps can import from.

// packages/shared-keys/index.ts
export const KEYS = {
  user: "user",
  cart: "cart",
  featureFlags: "feature-flags",
} as const;

Then import them wherever you use relay-state:

import { KEYS } from "@myorg/shared-keys";
import { useRelayState, useRelayStateValue } from "relay-state/react";

function Counter() {
  const [user, setUser] = useRelayState(KEYS.user);
  // ...
}

This gives you a single source of truth for the key namespace, makes refactoring safe (rename in one place), and makes it easy to see at a glance what state is shared across your application.

If you are using createStore for namespaced stores, the same principle applies -- centralize both the namespace string and the key names:

// packages/shared-keys/index.ts
export const STORES = {
  appA: "appA",
  appB: "appB",
} as const;

export const APP_A_KEYS = {
  user: "user",
  settings: "settings",
} as const;

How It Works

  1. State is stored in an in-memory Map -- fast reads and writes with zero serialization overhead.
  2. Every set and del call dispatches a CustomEvent on window with the event name relay-state:{key}.
  3. subscribe listens for these events using window.addEventListener. When an event arrives, it updates the local cache before calling the callback -- so get() always reflects the latest value, even if the event originated from a different bundle.
  4. useRelayState wires subscribe and get into React's useSyncExternalStore, so components re-render automatically.

Because events go through window, the underlying mechanism is framework-agnostic -- any script on the same page can listen. However, relay-state is designed and tested for React. Use in other frameworks is theoretically possible but unsupported.

Deploying Across Micro Frontends

relay-state works with independently bundled micro frontends — each app can include its own copy of the library. Updates propagate via window CustomEvents, and each bundle's local cache stays in sync when it receives an event.

single-spa

Each micro frontend installs relay-state as a normal dependency. No special configuration is required:

# pnpm
pnpm add relay-state

# npm
npm install relay-state

# Vite+
vp add relay-state

State written by one app is broadcast via window events and received by all other apps that have subscribed to the same key, regardless of which bundle they loaded relay-state from.

Optional: share a single instance via import maps

If you want all micro frontends to share one bundle of relay-state (slightly more efficient, one fewer module to download), you can register it as a shared dependency in your import map:

{
  "imports": {
    "relay-state": "https://cdn.example.com/[email protected]/index.mjs",
    "relay-state/react": "https://cdn.example.com/[email protected]/react.mjs"
  }
}

Then each micro frontend imports relay-state normally -- the browser resolves it to the shared CDN bundle instead of a local copy. With a shared instance, all apps also share the in-memory cache directly (not just via events), which eliminates any edge cases where a consumer reads get() before subscribing.

Future Ideas

These features are planned but not yet implemented:

Atom-level defaults. Today, initialValue is set per hook call. A future API would let you define the key, type, and default value together as an atom -- similar to Jotai -- so the default is co-located with the key definition and shared across all consumers automatically:

// future API (not yet implemented)
const countAtom = atom<number>("count", 0);

function Counter() {
  const [count, setCount] = useRelayState(countAtom);
  // count is always number, never undefined
}

Typed store. A createTypedStore API that encodes the full key-to-type map at the store level, giving compile-time safety without a separate constants file:

// future API (not yet implemented)
const store = createTypedStore<{
  user: { name: string; role: string };
  count: number;
}>("appA");

store.set("user", { name: "Leo", role: "admin" }); // fully typed
store.set("typo", 1); // TypeScript error

Not the Right Fit?

relay-state is purpose-built for cross-micro-frontend state sharing in React-based architectures like single-spa where independently deployed apps share a browser window. It is intentionally minimal and not a general-purpose state manager.

If you need a full-featured state management solution within a single React application, consider:

  • Zustand -- A small, fast, and scalable state management library for React. Great for app-level state with a simple hook-based API.
  • Jotai -- Primitive and flexible atomic state management for React. Ideal when you want fine-grained, bottom-up state composition.

Both are excellent choices for React application state. relay-state fills a different niche: lightweight state that needs to cross micro frontend boundaries.

Contributing

See CONTRIBUTING.md.

License

MIT © Leo Mendez