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 +21 -0
- package/README.md +359 -0
- package/dist/index.d.mts +54 -0
- package/dist/index.mjs +100 -0
- package/dist/react.d.mts +29 -0
- package/dist/react.mjs +39 -0
- package/package.json +60 -0
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
|
+
[](https://github.com/leomendez/relay-state/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.npmjs.com/package/relay-state)
|
|
7
|
+
[](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) © Leo Mendez
|
package/dist/index.d.mts
ADDED
|
@@ -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 };
|
package/dist/react.d.mts
ADDED
|
@@ -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
|
+
}
|