relay-state 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Leo Mendez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,359 @@
1
+ # relay-state
2
+
3
+ > Shared state for micro frontends, designed for React. Client-side only.
4
+
5
+ [![CI](https://github.com/leomendez/relay-state/actions/workflows/ci.yml/badge.svg)](https://github.com/leomendez/relay-state/actions/workflows/ci.yml)
6
+ [![npm](https://img.shields.io/npm/v/relay-state)](https://www.npmjs.com/package/relay-state)
7
+ [![license](https://img.shields.io/github/license/leomendez/relay-state)](LICENSE)
8
+
9
+ ## The Problem
10
+
11
+ In micro frontend architectures like [single-spa](https://single-spa.js.org/), 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.
12
+
13
+ Common workarounds -- module federation, custom event buses cobbled together per team, or dumping state into `localStorage` -- are either heavyweight, fragile, or require framework coupling.
14
+
15
+ **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.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ # pnpm
21
+ pnpm add relay-state
22
+
23
+ # npm
24
+ npm install relay-state
25
+
26
+ # Vite+
27
+ vp add relay-state
28
+ ```
29
+
30
+ ## Quick Start
31
+
32
+ ```tsx
33
+ import { useRelayState, useRelayStateValue, useSetRelayState } from "relay-state/react";
34
+
35
+ // Tuple API — like useState, but shared across micro frontends
36
+ function Counter() {
37
+ const [count, setCount] = useRelayState<number>("count", 0);
38
+ return <button onClick={() => setCount((n) => (n ?? 0) + 1)}>Count: {count}</button>;
39
+ }
40
+
41
+ // Read-only — re-renders on changes, no setter
42
+ function UserBadge() {
43
+ const user = useRelayStateValue<{ name: string; role: string }>("user");
44
+ if (!user) return null;
45
+ return (
46
+ <span>
47
+ {user.name} ({user.role})
48
+ </span>
49
+ );
50
+ }
51
+
52
+ // Write-only — does NOT re-render when state changes
53
+ function PromoteButton() {
54
+ const setUser = useSetRelayState<{ name: string; role: string }>("user");
55
+ return <button onClick={() => setUser((u) => ({ ...u, role: "admin" }))}>Promote</button>;
56
+ }
57
+ ```
58
+
59
+ ## API
60
+
61
+ ### `get<T>(key: string): T | undefined`
62
+
63
+ Returns the current value for a key, or `undefined` if the key has not been set.
64
+
65
+ ```ts
66
+ const count = get<number>("count"); // number | undefined
67
+ ```
68
+
69
+ ### `has(key: string): boolean`
70
+
71
+ 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."
72
+
73
+ ```ts
74
+ set("key", undefined);
75
+ has("key"); // true
76
+ has("other"); // false
77
+ ```
78
+
79
+ ### `set<T>(key: string, value: T | ((prev: T | undefined) => T)): void`
80
+
81
+ 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.
82
+
83
+ ```ts
84
+ // Direct value
85
+ set("count", 0);
86
+
87
+ // Updater function (receives the previous value)
88
+ set<number>("count", (prev) => (prev ?? 0) + 1);
89
+ ```
90
+
91
+ > **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.
92
+
93
+ ### `subscribe<T>(key: string, callback: (value: T | undefined) => void): () => void`
94
+
95
+ 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.
96
+
97
+ ```ts
98
+ const unsubscribe = subscribe<number>("count", (value) => {
99
+ console.log("Count is now:", value);
100
+ });
101
+
102
+ // Stop listening
103
+ unsubscribe();
104
+ ```
105
+
106
+ This signature is designed to work directly with React's [`useSyncExternalStore`](https://react.dev/reference/react/useSyncExternalStore).
107
+
108
+ ### `del(key: string): void`
109
+
110
+ Deletes a key from the store and notifies subscribers with `undefined`.
111
+
112
+ ```ts
113
+ del("count");
114
+ ```
115
+
116
+ ### `clear(): void`
117
+
118
+ Removes all keys from the store and notifies all active subscribers with `undefined`. Use this for logout flows or full application resets.
119
+
120
+ ```ts
121
+ clear();
122
+ ```
123
+
124
+ ### `createStore(namespace: string): RelayStore`
125
+
126
+ 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.
127
+
128
+ ```ts
129
+ import { createStore } from "relay-state";
130
+
131
+ // In App A
132
+ const appA = createStore("appA");
133
+ appA.set("user", { name: "Leo" });
134
+ appA.get("user"); // { name: "Leo" }
135
+
136
+ // In App B
137
+ const appB = createStore("appB");
138
+ appB.set("user", { name: "Maria" });
139
+ appB.get("user"); // { name: "Maria" }
140
+
141
+ // No collision -- these are stored as "appA:user" and "appB:user"
142
+ ```
143
+
144
+ 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.
145
+
146
+ ### `RelayStore` (type)
147
+
148
+ The interface returned by `createStore`:
149
+
150
+ ```ts
151
+ interface RelayStore {
152
+ get: <T = unknown>(key: string) => T | undefined;
153
+ has: (key: string) => boolean;
154
+ set: <T = unknown>(key: string, value: T | ((prev: T | undefined) => T)) => void;
155
+ subscribe: <T = unknown>(key: string, callback: (value: T | undefined) => void) => () => void;
156
+ del: (key: string) => void;
157
+ clear: () => void;
158
+ }
159
+ ```
160
+
161
+ ## React Integration
162
+
163
+ A React hook is available via the `relay-state/react` entrypoint. It uses [`useSyncExternalStore`](https://react.dev/reference/react/useSyncExternalStore) under the hood, so your components re-render automatically when shared state changes.
164
+
165
+ React 18+ is a peer dependency.
166
+
167
+ > **Note:** relay-state is client-side only. It requires `window` at runtime and is not designed for server-side rendering.
168
+
169
+ ### `useRelayState<T>(key, initialValue?) → [value, setter]`
170
+
171
+ The primary hook. Returns a tuple of the current value and a setter — the same pattern as React's `useState`.
172
+
173
+ ```tsx
174
+ import { useRelayState } from "relay-state/react";
175
+
176
+ function Counter() {
177
+ const [count, setCount] = useRelayState<number>("count", 0);
178
+ return <button onClick={() => setCount((prev) => (prev ?? 0) + 1)}>Count: {count}</button>;
179
+ }
180
+ ```
181
+
182
+ The setter accepts either a direct value or an updater function:
183
+
184
+ ```ts
185
+ setCount(10);
186
+ setCount((prev) => (prev ?? 0) + 1);
187
+ ```
188
+
189
+ 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.
190
+
191
+ ### `useRelayStateValue<T>(key, initialValue?) → value`
192
+
193
+ Subscribes to a key and returns only the current value. Use this when a component needs to read state but never write it.
194
+
195
+ ```tsx
196
+ import { useRelayStateValue } from "relay-state/react";
197
+
198
+ function UserBadge() {
199
+ const user = useRelayStateValue<{ name: string }>("user");
200
+ if (!user) return null;
201
+ return <span>{user.name}</span>;
202
+ }
203
+ ```
204
+
205
+ ### `useSetRelayState<T>(key) → setter`
206
+
207
+ 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.
208
+
209
+ ```tsx
210
+ import { useSetRelayState } from "relay-state/react";
211
+
212
+ function PromoteButton() {
213
+ const setUser = useSetRelayState<{ name: string; role: string }>("user");
214
+ return (
215
+ <button onClick={() => setUser((prev) => ({ ...prev, role: "admin" }))}>
216
+ Promote to Admin
217
+ </button>
218
+ );
219
+ }
220
+ ```
221
+
222
+ The `relay-state/react` entrypoint also re-exports all core functions (`get`, `has`, `set`, `del`, `subscribe`, `createStore`, `clear`) and the `RelayStore` type for convenience.
223
+
224
+ ## Best Practices
225
+
226
+ ### Centralize keys as constants
227
+
228
+ 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.
229
+
230
+ ```ts
231
+ // packages/shared-keys/index.ts
232
+ export const KEYS = {
233
+ user: "user",
234
+ cart: "cart",
235
+ featureFlags: "feature-flags",
236
+ } as const;
237
+ ```
238
+
239
+ Then import them wherever you use relay-state:
240
+
241
+ ```tsx
242
+ import { KEYS } from "@myorg/shared-keys";
243
+ import { useRelayState, useRelayStateValue } from "relay-state/react";
244
+
245
+ function Counter() {
246
+ const [user, setUser] = useRelayState(KEYS.user);
247
+ // ...
248
+ }
249
+ ```
250
+
251
+ 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.
252
+
253
+ If you are using `createStore` for namespaced stores, the same principle applies -- centralize both the namespace string and the key names:
254
+
255
+ ```ts
256
+ // packages/shared-keys/index.ts
257
+ export const STORES = {
258
+ appA: "appA",
259
+ appB: "appB",
260
+ } as const;
261
+
262
+ export const APP_A_KEYS = {
263
+ user: "user",
264
+ settings: "settings",
265
+ } as const;
266
+ ```
267
+
268
+ ## How It Works
269
+
270
+ 1. State is stored in an in-memory `Map` -- fast reads and writes with zero serialization overhead.
271
+ 2. Every `set` and `del` call dispatches a [`CustomEvent`](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) on `window` with the event name `relay-state:{key}`.
272
+ 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.
273
+ 4. `useRelayState` wires `subscribe` and `get` into React's `useSyncExternalStore`, so components re-render automatically.
274
+
275
+ 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.
276
+
277
+ ## Deploying Across Micro Frontends
278
+
279
+ 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.
280
+
281
+ ### single-spa
282
+
283
+ Each micro frontend installs relay-state as a normal dependency. No special configuration is required:
284
+
285
+ ```bash
286
+ # pnpm
287
+ pnpm add relay-state
288
+
289
+ # npm
290
+ npm install relay-state
291
+
292
+ # Vite+
293
+ vp add relay-state
294
+ ```
295
+
296
+ 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.
297
+
298
+ **Optional: share a single instance via import maps**
299
+
300
+ 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:
301
+
302
+ ```json
303
+ {
304
+ "imports": {
305
+ "relay-state": "https://cdn.example.com/relay-state@0.1.0/index.mjs",
306
+ "relay-state/react": "https://cdn.example.com/relay-state@0.1.0/react.mjs"
307
+ }
308
+ }
309
+ ```
310
+
311
+ 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.
312
+
313
+ ## Future Ideas
314
+
315
+ These features are planned but not yet implemented:
316
+
317
+ **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:
318
+
319
+ ```ts
320
+ // future API (not yet implemented)
321
+ const countAtom = atom<number>("count", 0);
322
+
323
+ function Counter() {
324
+ const [count, setCount] = useRelayState(countAtom);
325
+ // count is always number, never undefined
326
+ }
327
+ ```
328
+
329
+ **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:
330
+
331
+ ```ts
332
+ // future API (not yet implemented)
333
+ const store = createTypedStore<{
334
+ user: { name: string; role: string };
335
+ count: number;
336
+ }>("appA");
337
+
338
+ store.set("user", { name: "Leo", role: "admin" }); // fully typed
339
+ store.set("typo", 1); // TypeScript error
340
+ ```
341
+
342
+ ## Not the Right Fit?
343
+
344
+ 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.
345
+
346
+ If you need a full-featured state management solution within a single React application, consider:
347
+
348
+ - **[Zustand](https://github.com/pmndrs/zustand)** -- A small, fast, and scalable state management library for React. Great for app-level state with a simple hook-based API.
349
+ - **[Jotai](https://github.com/pmndrs/jotai)** -- Primitive and flexible atomic state management for React. Ideal when you want fine-grained, bottom-up state composition.
350
+
351
+ Both are excellent choices for React application state. relay-state fills a different niche: lightweight state that needs to cross micro frontend boundaries.
352
+
353
+ ## Contributing
354
+
355
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
356
+
357
+ ## License
358
+
359
+ [MIT](LICENSE) &copy; Leo Mendez
@@ -0,0 +1,54 @@
1
+ //#region src/index.d.ts
2
+ /**
3
+ * Sentinel value dispatched by `del` and `clear` to distinguish deletion from storing `undefined`.
4
+ * Uses `Symbol.for` so the sentinel is shared across independently bundled copies.
5
+ */
6
+ declare const DELETED: unique symbol;
7
+ /** Returns the current value for a key, or `undefined` if the key has not been set. */
8
+ declare function get<T = unknown>(key: string): T | undefined;
9
+ /** Returns `true` if the key exists in the store, even if its value is `undefined`. */
10
+ declare function has(key: string): boolean;
11
+ /**
12
+ * Sets a value in the store and dispatches a `CustomEvent` on `window` to notify all subscribers.
13
+ * Accepts either a direct value or an updater function that receives the previous value.
14
+ *
15
+ * **Note:** Because updater functions are detected via `typeof value === "function"`, storing a
16
+ * function as a value requires wrapping it: `set("cb", () => myFunction)`.
17
+ */
18
+ declare function set<T = unknown>(key: string, value: T | ((prev: T | undefined) => T)): void;
19
+ /**
20
+ * Subscribes to changes for a key. The callback fires whenever `set` or `del` is called for
21
+ * that key, including updates originating from other micro frontend bundles. Returns an
22
+ * unsubscribe function.
23
+ *
24
+ * The signature is compatible with React's `useSyncExternalStore`.
25
+ */
26
+ declare function subscribe<T = unknown>(key: string, callback: (value: T | undefined) => void): () => void;
27
+ /** Deletes a key from the store and notifies subscribers with `undefined`. */
28
+ declare function del(key: string): void;
29
+ /**
30
+ * Removes all keys from the store and notifies all active subscribers with `undefined`.
31
+ * Useful for logout flows or full application resets.
32
+ */
33
+ declare function clear(): void;
34
+ /** The interface returned by `createStore`. */
35
+ interface RelayStore {
36
+ get: <T = unknown>(key: string) => T | undefined;
37
+ has: (key: string) => boolean;
38
+ set: <T = unknown>(key: string, value: T | ((prev: T | undefined) => T)) => void;
39
+ subscribe: <T = unknown>(key: string, callback: (value: T | undefined) => void) => () => void;
40
+ del: (key: string) => void;
41
+ /** Removes all keys in this namespace and notifies their subscribers with `undefined`. */
42
+ clear: () => void;
43
+ }
44
+ /**
45
+ * Creates a namespaced store. All keys are prefixed with `namespace:` internally, preventing
46
+ * collisions between micro frontends while sharing the same underlying cache and event bus.
47
+ *
48
+ * @example
49
+ * const appA = createStore("appA");
50
+ * appA.set("user", { name: "Leo" }); // stored as "appA:user"
51
+ */
52
+ declare function createStore(namespace: string): RelayStore;
53
+ //#endregion
54
+ export { DELETED, RelayStore, clear, createStore, del, get, has, set, subscribe };
package/dist/index.mjs ADDED
@@ -0,0 +1,100 @@
1
+ //#region src/index.ts
2
+ const cache = /* @__PURE__ */ new Map();
3
+ const EVENT_PREFIX = "relay-state:";
4
+ /**
5
+ * Sentinel value dispatched by `del` and `clear` to distinguish deletion from storing `undefined`.
6
+ * Uses `Symbol.for` so the sentinel is shared across independently bundled copies.
7
+ */
8
+ const DELETED = Symbol.for("relay-state:deleted");
9
+ function dispatch(key, value) {
10
+ window.dispatchEvent(new CustomEvent(`${EVENT_PREFIX}${key}`, { detail: { value } }));
11
+ }
12
+ /** Returns the current value for a key, or `undefined` if the key has not been set. */
13
+ function get(key) {
14
+ return cache.get(key);
15
+ }
16
+ /** Returns `true` if the key exists in the store, even if its value is `undefined`. */
17
+ function has(key) {
18
+ return cache.has(key);
19
+ }
20
+ /**
21
+ * Sets a value in the store and dispatches a `CustomEvent` on `window` to notify all subscribers.
22
+ * Accepts either a direct value or an updater function that receives the previous value.
23
+ *
24
+ * **Note:** Because updater functions are detected via `typeof value === "function"`, storing a
25
+ * function as a value requires wrapping it: `set("cb", () => myFunction)`.
26
+ */
27
+ function set(key, value) {
28
+ const resolved = typeof value === "function" ? value(cache.get(key)) : value;
29
+ if (cache.has(key) && Object.is(cache.get(key), resolved)) return;
30
+ cache.set(key, resolved);
31
+ dispatch(key, resolved);
32
+ }
33
+ /**
34
+ * Subscribes to changes for a key. The callback fires whenever `set` or `del` is called for
35
+ * that key, including updates originating from other micro frontend bundles. Returns an
36
+ * unsubscribe function.
37
+ *
38
+ * The signature is compatible with React's `useSyncExternalStore`.
39
+ */
40
+ function subscribe(key, callback) {
41
+ const listener = (event) => {
42
+ const val = event.detail.value;
43
+ if (val === DELETED) {
44
+ cache.delete(key);
45
+ callback(void 0);
46
+ } else {
47
+ if (!cache.has(key) || !Object.is(cache.get(key), val)) cache.set(key, val);
48
+ callback(val);
49
+ }
50
+ };
51
+ window.addEventListener(`${EVENT_PREFIX}${key}`, listener);
52
+ if (cache.has(key)) callback(cache.get(key));
53
+ return () => {
54
+ window.removeEventListener(`${EVENT_PREFIX}${key}`, listener);
55
+ };
56
+ }
57
+ /** Deletes a key from the store and notifies subscribers with `undefined`. */
58
+ function del(key) {
59
+ cache.delete(key);
60
+ dispatch(key, DELETED);
61
+ }
62
+ /**
63
+ * Removes all keys from the store and notifies all active subscribers with `undefined`.
64
+ * Useful for logout flows or full application resets.
65
+ */
66
+ function clear() {
67
+ for (const key of Array.from(cache.keys())) del(key);
68
+ }
69
+ /**
70
+ * Creates a namespaced store. All keys are prefixed with `namespace:` internally, preventing
71
+ * collisions between micro frontends while sharing the same underlying cache and event bus.
72
+ *
73
+ * @example
74
+ * const appA = createStore("appA");
75
+ * appA.set("user", { name: "Leo" }); // stored as "appA:user"
76
+ */
77
+ function createStore(namespace) {
78
+ const prefix = (key) => `${namespace}:${key}`;
79
+ const keys = /* @__PURE__ */ new Set();
80
+ return {
81
+ get: (key) => get(prefix(key)),
82
+ has: (key) => has(prefix(key)),
83
+ set: (key, value) => {
84
+ keys.add(key);
85
+ set(prefix(key), value);
86
+ },
87
+ subscribe: (key, callback) => subscribe(prefix(key), callback),
88
+ del: (key) => {
89
+ keys.delete(key);
90
+ del(prefix(key));
91
+ },
92
+ clear: () => {
93
+ for (const key of keys) del(prefix(key));
94
+ keys.clear();
95
+ }
96
+ };
97
+ }
98
+ if (typeof window !== "undefined" && import.meta.env?.DEV) window.__RELAY_STATE__ = { cache };
99
+ //#endregion
100
+ export { DELETED, clear, createStore, del, get, has, set, subscribe };
@@ -0,0 +1,29 @@
1
+ import { DELETED, RelayStore, clear, createStore, del, get, has, set, subscribe } from "./index.mjs";
2
+
3
+ //#region src/react.d.ts
4
+ /** A setter that accepts either a direct value or an updater function. */
5
+ type Setter<T> = (value: T | ((prev: T | undefined) => T)) => void;
6
+ /**
7
+ * Subscribes to a key and returns the current value. Re-renders when the value changes.
8
+ *
9
+ * If `initialValue` is provided and the key is currently unset, the value is written to the
10
+ * store on first mount so all consumers see the same default.
11
+ *
12
+ * Use this when a component needs to read state but never write it.
13
+ */
14
+ declare function useRelayStateValue<T = unknown>(key: string, initialValue?: T): T | undefined;
15
+ /**
16
+ * Returns a stable setter for a key. The component does **not** re-render when the value
17
+ * changes — the key performance primitive for write-only components.
18
+ */
19
+ declare function useSetRelayState<T = unknown>(key: string): Setter<T>;
20
+ /**
21
+ * Returns a `[value, setter]` tuple — the same pattern as React's `useState`, but shared
22
+ * across micro frontends. Re-renders when the value changes.
23
+ *
24
+ * If `initialValue` is provided and the key is currently unset, the value is written to the
25
+ * store on first mount so all consumers see the same default.
26
+ */
27
+ declare function useRelayState<T = unknown>(key: string, initialValue?: T): [T | undefined, Setter<T>];
28
+ //#endregion
29
+ export { DELETED, type RelayStore, Setter, clear, createStore, del, get, has, set, subscribe, useRelayState, useRelayStateValue, useSetRelayState };
package/dist/react.mjs ADDED
@@ -0,0 +1,39 @@
1
+ import { DELETED, clear, createStore, del, get, has, set, subscribe } from "./index.mjs";
2
+ import { useCallback, useRef, useSyncExternalStore } from "react";
3
+ //#region src/react.ts
4
+ /**
5
+ * Subscribes to a key and returns the current value. Re-renders when the value changes.
6
+ *
7
+ * If `initialValue` is provided and the key is currently unset, the value is written to the
8
+ * store on first mount so all consumers see the same default.
9
+ *
10
+ * Use this when a component needs to read state but never write it.
11
+ */
12
+ function useRelayStateValue(key, initialValue) {
13
+ const initKeyRef = useRef(null);
14
+ if (initKeyRef.current !== key) {
15
+ initKeyRef.current = key;
16
+ if (initialValue !== void 0 && get(key) === void 0) set(key, initialValue);
17
+ }
18
+ const value = useSyncExternalStore((cb) => subscribe(key, cb), () => get(key), () => get(key));
19
+ return value === void 0 ? initialValue : value;
20
+ }
21
+ /**
22
+ * Returns a stable setter for a key. The component does **not** re-render when the value
23
+ * changes — the key performance primitive for write-only components.
24
+ */
25
+ function useSetRelayState(key) {
26
+ return useCallback((value) => set(key, value), [key]);
27
+ }
28
+ /**
29
+ * Returns a `[value, setter]` tuple — the same pattern as React's `useState`, but shared
30
+ * across micro frontends. Re-renders when the value changes.
31
+ *
32
+ * If `initialValue` is provided and the key is currently unset, the value is written to the
33
+ * store on first mount so all consumers see the same default.
34
+ */
35
+ function useRelayState(key, initialValue) {
36
+ return [useRelayStateValue(key, initialValue), useSetRelayState(key)];
37
+ }
38
+ //#endregion
39
+ export { DELETED, clear, createStore, del, get, has, set, subscribe, useRelayState, useRelayStateValue, useSetRelayState };
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "relay-state",
3
+ "version": "0.2.0",
4
+ "description": "Framework-agnostic shared state store for micro frontends, powered by window events and an in-memory cache.",
5
+ "homepage": "https://github.com/leomendez/relay-state#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/leomendez/relay-state/issues"
8
+ },
9
+ "license": "MIT",
10
+ "author": "Leo Mendez",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/leomendez/relay-state.git"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "type": "module",
19
+ "exports": {
20
+ ".": "./dist/index.mjs",
21
+ "./react": "./dist/react.mjs",
22
+ "./package.json": "./package.json"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "scripts": {
28
+ "build": "vp pack",
29
+ "dev": "vp pack --watch",
30
+ "test": "vp test",
31
+ "check": "vp check",
32
+ "prepublishOnly": "vp run build",
33
+ "prepare": "vp config"
34
+ },
35
+ "devDependencies": {
36
+ "@testing-library/react": "^16.3.2",
37
+ "@types/node": "^25.5.0",
38
+ "@types/react": "^19.2.14",
39
+ "@types/react-dom": "^19.2.3",
40
+ "@typescript/native-preview": "7.0.0-dev.20260328.1",
41
+ "bumpp": "^11.0.1",
42
+ "jsdom": "^29.0.1",
43
+ "react": "^19.2.4",
44
+ "react-dom": "^19.2.4",
45
+ "typescript": "^6.0.2",
46
+ "vite-plus": "^0.1.14"
47
+ },
48
+ "peerDependencies": {
49
+ "react": ">=18.0.0"
50
+ },
51
+ "peerDependenciesMeta": {
52
+ "react": {
53
+ "optional": true
54
+ }
55
+ },
56
+ "engines": {
57
+ "node": ">=20.0.0"
58
+ },
59
+ "packageManager": "pnpm@10.33.0"
60
+ }