@yakocloud/state-vocab 4.0.2 → 4.0.3

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/README.md CHANGED
@@ -260,6 +260,81 @@ By default `ssr: true`:
260
260
 
261
261
  This guarantees the server and client produce identical markup, and the value from storage is applied without a visible flash.
262
262
 
263
+ ### Next.js Pages Router (SSR without RSC)
264
+
265
+ If you use Next.js with the **Pages Router** (or any SSR setup without React Server Components), you still need `ssr: true` to prevent hydration mismatches — but you don't have server components to wrap with `StateVocabProvider` from `serverify`.
266
+
267
+ In this case, wrap your app with `StateVocabClientProvider` from `@yakocloud/state-vocab/client`. It creates an isolated `VocabStore` per render, preventing state from leaking between concurrent SSR requests.
268
+
269
+ ```tsx
270
+ // pages/_app.tsx
271
+ import type { AppProps } from 'next/app'
272
+ import { StateVocabClientProvider } from '@yakocloud/state-vocab/client'
273
+
274
+ export default function App({ Component, pageProps }: AppProps) {
275
+ return (
276
+ <StateVocabClientProvider>
277
+ <Component {...pageProps} />
278
+ </StateVocabClientProvider>
279
+ )
280
+ }
281
+ ```
282
+
283
+ Storage is still configured with `ssr: true` (the default):
284
+
285
+ ```ts
286
+ // lib/storage.ts
287
+ import { setupStorage, defineState } from '@yakocloud/state-vocab'
288
+ import { clientify } from '@yakocloud/state-vocab/client'
289
+
290
+ const storage = setupStorage({
291
+ preference: {
292
+ theme: defineState<'Dark' | 'White' | 'System'>({
293
+ storage: localStorage,
294
+ defaultValue: 'Dark',
295
+ }),
296
+ },
297
+ // ssr: true is the default — storage reads are deferred until after hydration
298
+ })
299
+
300
+ export const clientStorage = clientify(storage)
301
+ ```
302
+
303
+ Use `clientStorage` directly in any page or component — no additional wiring needed:
304
+
305
+ ```tsx
306
+ // pages/settings.tsx
307
+ import { clientStorage } from '@/lib/storage'
308
+
309
+ export default function Settings() {
310
+ const [theme, setTheme] = clientStorage.preference.theme.useState()
311
+
312
+ return (
313
+ <select value={theme} onChange={(e) => setTheme(e.target.value as 'Dark' | 'White' | 'System')}>
314
+ <option value="Dark">Dark</option>
315
+ <option value="White">White</option>
316
+ <option value="System">System</option>
317
+ </select>
318
+ )
319
+ }
320
+ ```
321
+
322
+ `StateVocabClientProvider` also accepts an optional `value` prop to pre-seed the store with server-fetched data (e.g., from `getServerSideProps`):
323
+
324
+ ```tsx
325
+ // pages/_app.tsx
326
+ import type { AppProps } from 'next/app'
327
+ import { StateVocabClientProvider } from '@yakocloud/state-vocab/client'
328
+
329
+ export default function App({ Component, pageProps }: AppProps) {
330
+ return (
331
+ <StateVocabClientProvider value={pageProps.initialVocab}>
332
+ <Component {...pageProps} />
333
+ </StateVocabClientProvider>
334
+ )
335
+ }
336
+ ```
337
+
263
338
  ### React Server Components (RSC)
264
339
 
265
340
  For Next.js apps using the App Router, state-vocab provides dedicated server and client entry points that let you read state in async server components and pass it down to client components via `StateVocabProvider`.
@@ -270,7 +345,7 @@ For Next.js apps using the App Router, state-vocab provides dedicated server and
270
345
  |---|---|
271
346
  | `@yakocloud/state-vocab` | Shared files — `defineState`, `setupStorage` |
272
347
  | `@yakocloud/state-vocab/server` | Server Components — `serverify` |
273
- | `@yakocloud/state-vocab/client` | Client Components — `clientify` |
348
+ | `@yakocloud/state-vocab/client` | Client Components — `clientify`, `StateVocabClientProvider` |
274
349
 
275
350
  **1. Define the shared storage schema** (used on both server and client):
276
351
 
@@ -607,6 +682,18 @@ const { StateVocabProvider } = serverStorage
607
682
  </StateVocabProvider>
608
683
  ```
609
684
 
685
+ ### `StateVocabClientProvider`
686
+
687
+ A client-only provider for SSR setups **without** React Server Components (e.g. Next.js Pages Router). Import from `@yakocloud/state-vocab/client` and place it at your app root to ensure per-request store isolation. Accepts an optional `value` prop to pre-seed the store.
688
+
689
+ ```tsx
690
+ import { StateVocabClientProvider } from '@yakocloud/state-vocab/client'
691
+
692
+ <StateVocabClientProvider value={initialVocab}>
693
+ <App />
694
+ </StateVocabClientProvider>
695
+ ```
696
+
610
697
  ### `serverify<T>(storage: T)`
611
698
 
612
699
  Converts a storage tree to its server-side counterpart. Available from `@yakocloud/state-vocab/server`.
@@ -1 +1 @@
1
- "use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const u=require("react"),v=require("./constants-CbsduCZ7.js"),a=require("./utils-0CTNJ4ZE.js"),y=require("./provider-client-B4XQ24JB.js");function D(t,e,n=[]){return u.useMemo(()=>a.debounce(t,e),n)}const g=t=>{const{vocabStore:e,storage:n,statePath:o,value:s,deserialize:S,serialize:l}=t,f=n.getItem(o);f===null?a.isValueDefined(s)&&n.setItem(o,l(s)):e.set(o,S(f))},z=typeof window>"u",m=z?u.useEffect:u.useLayoutEffect,A=new y.VocabStore,C="Make sure your component is wrapped in StateVocabProvider (RSC) or disable ssr option in setupStorage for SPA (RCC only)";function I(t){t??={};const e=z?void 0:a.valueOrFactory(t.storage),n=a.valueOrFactory(t.defaultValue),o=t.bidirectional,s=this[v.STATE_PATH],S=this[v.STATE_VERBOSE],l=this[v.STATE_VERBOSE_PATH],f=this[v.STATE_SSR];let i=y.useStateVocabClientContext({verbose:S});if(!(i instanceof y.VocabStore)){if(f)throw new Error(C);i=A}const d=t.serialize??JSON.stringify,c=t.deserialize??JSON.parse,V=D(t.onSet??(()=>{}),t.delayedSet,[]),E=u.useRef(void 0),w=u.useRef(!1);if(!w.current){w.current=!0;let r=i.get(s);a.isValueDefined(r)||(r=n,a.isValueDefined(r)&&i.set(s,r)),!f&&e&&g({vocabStore:i,storage:e,statePath:s,value:r,serialize:d,deserialize:c})}const h=u.useSyncExternalStore(i.subscribe.bind(i),i.getClientSnapshot.bind(i),i.getServerSnapshot.bind(i));if(S)if(l){const r=a.get(h,l);r&&a.logStyled(r)}else a.logStyled(h);const T=a.get(h,s,n);E.current=T,m(()=>{!f||!e||g({vocabStore:i,storage:e,statePath:s,value:T,serialize:d,deserialize:c})},[]);const O=u.useEffectEvent(r=>{if(r.key!==s)return;const b=r.newValue,R=(b===null?null:c(b))??n;a.isValueDefined(R)&&(i.set(s,R),V(R,E.current))});u.useEffect(()=>{if(o)return window.addEventListener("storage",O),()=>window.removeEventListener("storage",O)},[o]);const p=u.useCallback(r=>{const b=a.isTransformer(r)?r(E.current):r;i.set(s,b),V(b,E.current),e&&e.setItem(s,d(b))},[i,s,V,e,d]),_=u.useCallback(()=>{const r=n;if(!a.isValueDefined(r)){e?.removeItem(s);return}i.set(s,r),V(r,E.current),e&&e.setItem(s,d(r))},[n,i,s,V,e,d]);return[T,p,_]}function k(t){t??={};const e=z?void 0:a.valueOrFactory(t.storage),n=a.valueOrFactory(t.defaultValue),o=this[v.STATE_PATH],s=this[v.STATE_VERBOSE],S=this[v.STATE_SSR];let l=y.useStateVocabClientContext({verbose:s});if(!(l instanceof y.VocabStore)){if(S)throw new Error(C);l=A}const f=t.serialize??JSON.stringify,i=t.deserialize??JSON.parse,d=u.useRef(!1);let c;d.current||(d.current=!0,c=l.get(o),a.isValueDefined(c)||(c=n,a.isValueDefined(c)&&l.set(o,c)),!S&&e&&g({vocabStore:l,storage:e,statePath:o,value:c,serialize:f,deserialize:i})),m(()=>{!S||!e||g({vocabStore:l,storage:e,statePath:o,value:c,serialize:f,deserialize:i})},[])}function q(t){return typeof t=="object"&&t!==null&&"clientSlot"in t}function P(t){const e={};for(const n in t){const o=t[n];q(o)?(e[n]=o.clientSlot({useState(...s){return I.apply(this,s)},useInitialState(...s){k.apply(this,s)}}),delete e[n].serverSlot,delete e[n].clientSlot):o!==null&&typeof o=="object"?e[n]=P(o):e[n]=o}return e}exports.clientify=P;
1
+ "use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const u=require("react"),v=require("./constants-CbsduCZ7.js"),a=require("./utils-0CTNJ4ZE.js"),V=require("./provider-client-B4XQ24JB.js");function D(t,e,n=[]){return u.useMemo(()=>a.debounce(t,e),n)}const g=t=>{const{vocabStore:e,storage:n,statePath:o,value:i,deserialize:S,serialize:l}=t,f=n.getItem(o);f===null?a.isValueDefined(i)&&n.setItem(o,l(i)):e.set(o,S(f))},z=typeof window>"u",O=z?u.useEffect:u.useLayoutEffect,m=new V.VocabStore,A="Make sure your component is wrapped in StateVocabProvider (RSC) or disable ssr option in setupStorage for SPA (RCC only)";function I(t){t??={};const e=z?void 0:a.valueOrFactory(t.storage),n=a.valueOrFactory(t.defaultValue),o=t.bidirectional,i=this[v.STATE_PATH],S=this[v.STATE_VERBOSE],l=this[v.STATE_VERBOSE_PATH],f=this[v.STATE_SSR];let s=V.useStateVocabClientContext({verbose:S});if(!(s instanceof V.VocabStore)){if(f)throw new Error(A);s=m}const d=t.serialize??JSON.stringify,c=t.deserialize??JSON.parse,E=D(t.onSet??(()=>{}),t.delayedSet,[]),y=u.useRef(void 0),C=u.useRef(!1);if(!C.current){C.current=!0;let r=s.get(i);a.isValueDefined(r)||(r=n,a.isValueDefined(r)&&s.set(i,r)),!f&&e&&g({vocabStore:s,storage:e,statePath:i,value:r,serialize:d,deserialize:c})}const h=u.useSyncExternalStore(s.subscribe.bind(s),s.getClientSnapshot.bind(s),s.getServerSnapshot.bind(s));if(S)if(l){const r=a.get(h,l);r&&a.logStyled(r)}else a.logStyled(h);const T=a.get(h,i,n);y.current=T,O(()=>{!f||!e||g({vocabStore:s,storage:e,statePath:i,value:T,serialize:d,deserialize:c})},[]);const w=u.useEffectEvent(r=>{if(r.key!==i)return;const b=r.newValue,R=(b===null?null:c(b))??n;a.isValueDefined(R)&&(s.set(i,R),E(R,y.current))});u.useEffect(()=>{if(o)return window.addEventListener("storage",w),()=>window.removeEventListener("storage",w)},[o]);const p=u.useCallback(r=>{const b=a.isTransformer(r)?r(y.current):r;s.set(i,b),E(b,y.current),e&&e.setItem(i,d(b))},[s,i,E,e,d]),_=u.useCallback(()=>{const r=n;if(!a.isValueDefined(r)){e?.removeItem(i);return}s.set(i,r),E(r,y.current),e&&e.setItem(i,d(r))},[n,s,i,E,e,d]);return[T,p,_]}function k(t){t??={};const e=z?void 0:a.valueOrFactory(t.storage),n=a.valueOrFactory(t.defaultValue),o=this[v.STATE_PATH],i=this[v.STATE_VERBOSE],S=this[v.STATE_SSR];let l=V.useStateVocabClientContext({verbose:i});if(!(l instanceof V.VocabStore)){if(S)throw new Error(A);l=m}const f=t.serialize??JSON.stringify,s=t.deserialize??JSON.parse,d=u.useRef(!1);let c;d.current||(d.current=!0,c=l.get(o),a.isValueDefined(c)||(c=n,a.isValueDefined(c)&&l.set(o,c)),!S&&e&&g({vocabStore:l,storage:e,statePath:o,value:c,serialize:f,deserialize:s})),O(()=>{!S||!e||g({vocabStore:l,storage:e,statePath:o,value:c,serialize:f,deserialize:s})},[])}function q(t){return typeof t=="object"&&t!==null&&"clientSlot"in t}function P(t){const e={};for(const n in t){const o=t[n];q(o)?(e[n]=o.clientSlot({useState(...i){return I.apply(this,i)},useInitialState(...i){k.apply(this,i)}}),delete e[n].serverSlot,delete e[n].clientSlot):o!==null&&typeof o=="object"?e[n]=P(o):e[n]=o}return e}exports.StateVocabClientProvider=V.StateVocabClientProvider;exports.clientify=P;
package/dist/client.es.js CHANGED
@@ -1,10 +1,11 @@
1
1
  "use client";
2
- import { useMemo as B, useRef as V, useSyncExternalStore as D, useEffectEvent as H, useEffect as I, useCallback as T, useLayoutEffect as x } from "react";
3
- import { S as O, a as P, b as _, c as F } from "./constants-BB1YAX6c.mjs";
4
- import { a as d, d as G, v as g, g as A, l as C, i as q } from "./utils-xV3x3fTc.mjs";
2
+ import { useMemo as x, useRef as y, useSyncExternalStore as B, useEffectEvent as D, useEffect as A, useCallback as T, useLayoutEffect as H } from "react";
3
+ import { S as I, a as O, b as _, c as F } from "./constants-BB1YAX6c.mjs";
4
+ import { a as d, d as G, v as g, g as C, l as P, i as q } from "./utils-xV3x3fTc.mjs";
5
5
  import { V as z, u as k } from "./provider-client-C2Aw0Lmi.mjs";
6
+ import { S as ie } from "./provider-client-C2Aw0Lmi.mjs";
6
7
  function K(t, e, n = []) {
7
- return B(
8
+ return x(
8
9
  () => G(t, e),
9
10
  // eslint-disable-next-line react-hooks/exhaustive-deps
10
11
  n
@@ -20,10 +21,10 @@ const E = (t) => {
20
21
  serialize: a
21
22
  } = t, c = n.getItem(o);
22
23
  c === null ? d(s) && n.setItem(o, a(s)) : e.set(o, f(c));
23
- }, w = typeof window > "u", J = w ? I : x, L = new z(), N = "Make sure your component is wrapped in StateVocabProvider (RSC) or disable ssr option in setupStorage for SPA (RCC only)";
24
+ }, p = typeof window > "u", J = p ? A : H, L = new z(), N = "Make sure your component is wrapped in StateVocabProvider (RSC) or disable ssr option in setupStorage for SPA (RCC only)";
24
25
  function Q(t) {
25
26
  t ??= {};
26
- const e = w ? void 0 : g(t.storage), n = g(t.defaultValue), o = t.bidirectional, s = this[O], f = this[P], a = this[F], c = this[_];
27
+ const e = p ? void 0 : g(t.storage), n = g(t.defaultValue), o = t.bidirectional, s = this[I], f = this[O], a = this[F], c = this[_];
27
28
  let r = k({ verbose: f });
28
29
  if (!(r instanceof z)) {
29
30
  if (c)
@@ -35,9 +36,9 @@ function Q(t) {
35
36
  }),
36
37
  t.delayedSet,
37
38
  []
38
- ), b = V(void 0), p = V(!1);
39
- if (!p.current) {
40
- p.current = !0;
39
+ ), b = y(void 0), w = y(!1);
40
+ if (!w.current) {
41
+ w.current = !0;
41
42
  let i = r.get(s);
42
43
  d(i) || (i = n, d(i) && r.set(s, i)), !c && e && E({
43
44
  vocabStore: r,
@@ -48,18 +49,18 @@ function Q(t) {
48
49
  deserialize: l
49
50
  });
50
51
  }
51
- const h = D(
52
+ const h = B(
52
53
  r.subscribe.bind(r),
53
54
  r.getClientSnapshot.bind(r),
54
55
  r.getServerSnapshot.bind(r)
55
56
  );
56
57
  if (f)
57
58
  if (a) {
58
- const i = A(h, a);
59
- i && C(i);
59
+ const i = C(h, a);
60
+ i && P(i);
60
61
  } else
61
- C(h);
62
- const m = A(h, s, n);
62
+ P(h);
63
+ const m = C(h, s, n);
63
64
  b.current = m, J(() => {
64
65
  !c || !e || E({
65
66
  vocabStore: r,
@@ -70,13 +71,13 @@ function Q(t) {
70
71
  deserialize: l
71
72
  });
72
73
  }, []);
73
- const R = H((i) => {
74
+ const R = D((i) => {
74
75
  if (i.key !== s)
75
76
  return;
76
- const S = i.newValue, y = (S === null ? null : l(S)) ?? n;
77
- d(y) && (r.set(s, y), v(y, b.current));
77
+ const S = i.newValue, V = (S === null ? null : l(S)) ?? n;
78
+ d(V) && (r.set(s, V), v(V, b.current));
78
79
  });
79
- I(() => {
80
+ A(() => {
80
81
  if (o)
81
82
  return window.addEventListener("storage", R), () => window.removeEventListener("storage", R);
82
83
  }, [o]);
@@ -99,14 +100,14 @@ function Q(t) {
99
100
  }
100
101
  function U(t) {
101
102
  t ??= {};
102
- const e = w ? void 0 : g(t.storage), n = g(t.defaultValue), o = this[O], s = this[P], f = this[_];
103
+ const e = p ? void 0 : g(t.storage), n = g(t.defaultValue), o = this[I], s = this[O], f = this[_];
103
104
  let a = k({ verbose: s });
104
105
  if (!(a instanceof z)) {
105
106
  if (f)
106
107
  throw new Error(N);
107
108
  a = L;
108
109
  }
109
- const c = t.serialize ?? JSON.stringify, r = t.deserialize ?? JSON.parse, u = V(!1);
110
+ const c = t.serialize ?? JSON.stringify, r = t.deserialize ?? JSON.parse, u = y(!1);
110
111
  let l;
111
112
  u.current || (u.current = !0, l = a.get(o), d(l) || (l = n, d(l) && a.set(o, l)), !f && e && E({
112
113
  vocabStore: a,
@@ -145,5 +146,6 @@ function X(t) {
145
146
  return e;
146
147
  }
147
148
  export {
149
+ ie as StateVocabClientProvider,
148
150
  X as clientify
149
151
  };
@@ -1 +1,2 @@
1
1
  export { clientify } from "./setup.client";
2
+ export { StateVocabClientProvider } from "./provider.client";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yakocloud/state-vocab",
3
- "version": "4.0.2",
3
+ "version": "4.0.3",
4
4
  "main": "dist/index.cjs.js",
5
5
  "module": "dist/index.es.js",
6
6
  "types": "dist/types/index.d.ts",