@t8/react-store 1.1.3 → 1.2.1

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
@@ -4,6 +4,11 @@
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@t8/react-store?labelColor=345&color=46e)](https://www.npmjs.com/package/@t8/react-store) ![Lightweight](https://img.shields.io/bundlephobia/minzip/@t8/react-store?label=minzip&labelColor=345&color=46e) ![CSR ✓](https://img.shields.io/badge/CSR-✓-345?labelColor=345) ![SSR ✓](https://img.shields.io/badge/SSR-✓-345?labelColor=345)
6
6
 
7
+ **Why?** To have an easy-to-use state management lib for React apps requiring least effort to migrate from local state and to quickly set up shared state from scratch, whether with SSR or without. Other approaches, including Redux Toolkit, Zustand, Jotai, MobX, invariably depart from this picture to varying degrees.
8
+
9
+ This picture is achieved here by (1) having a simple API introducing as few new entities as possible, (2) closely following the React's `useState()` pattern of initializing and manipulating the state to avoid boilerplate and sizable rewrites in the common task of migration from local state to shared state, (3) working smoothly with SSR with regular React Contexts without requiring a specifically designed setup and without internally making use of global stores or other global variables by default.
10
+
11
+ <!-- docsgen-show-start --
7
12
  ```diff
8
13
  + let store = new Store(0);
9
14
 
@@ -18,12 +23,7 @@
18
23
  return <button onClick={handleClick}>+ {counter}</button>;
19
24
  };
20
25
  ```
21
-
22
- - Similar to `useState()`
23
- - Quick transition from local state
24
- - No boilerplate
25
- - Easily integrates with Immer
26
- - SSR- and CSR-compatible
26
+ -- docsgen-show-end -->
27
27
 
28
28
  Installation: `npm i @t8/react-store`
29
29
 
@@ -171,16 +171,18 @@ Immer can be used with `useStore()` just the same way as [with `useState()`](htt
171
171
 
172
172
  The ready-to-use hook from the [T8 React Pending](https://github.com/t8js/react-pending) package helps manage shared async action state without disturbing the app's state management and actions' code.
173
173
 
174
- ## Remount-persistent state
174
+ ## Persistence across remounts
175
175
 
176
176
  A standalone store initialized outside a component can be used by the component as remount-persistent state, whether used by other components or not.
177
177
 
178
178
  ## Persistence across page reloads
179
179
 
180
180
  ```js
181
- import { Store, persist } from "@t8/react-store";
181
+ import { PersistentStore } from "@t8/react-store";
182
182
 
183
- let counterStore = persist(new Store(0), "counter");
183
+ let counterStore = new PersistentStore(0, "counter");
184
184
  ```
185
185
 
186
- Whenever it's updated, `counterStore` above will save its state to the `"counter"` key of `localStorage`. (Pass `true` as the third parameter of `persist()` to use `sessionStorage` instead of `localStorage`.) `counterStore` returned from `persist()` is the same store passed as the first parameter enhanced to be persistent across page reloads.
186
+ The store's state value is initially restored from and saved whenever updated to the `"counter"` key of `localStorage`. (Pass `{ session: true }` as the `options` parameter of `new PersistentStore(data, storageKey, options?)` to use `sessionStorage` instead of `localStorage`.) Otherwise, `counterStore` works pretty much like a regular store described above.
187
+
188
+ The way data gets saved to and restored from a browser storage entry (including filtering out certain data or otherwise rearranging the saved data) can be overridden by setting `options.serialize` and `options.deserialize` in `new PersistentStore(data, storageKey, options?)`. By default, they are `JSON.stringify()` and `JSON.parse()`.
package/dist/index.js CHANGED
@@ -1,75 +1,26 @@
1
1
  // node_modules/@t8/store/src/isStore.ts
2
2
  function isStore(x) {
3
- return x !== null && typeof x === "object" && "on" in x && typeof x.on === "function" && "getState" in x && typeof x.getState === "function" && "setState" in x && typeof x.setState === "function";
3
+ return x !== null && typeof x === "object" && "onUpdate" in x && typeof x.onUpdate === "function" && "getState" in x && typeof x.getState === "function" && "setState" in x && typeof x.setState === "function";
4
4
  }
5
5
 
6
- // node_modules/@t8/store/src/persist.ts
7
- function getStorage(session) {
8
- if (typeof window === "undefined") return;
9
- return session ? sessionStorage : localStorage;
10
- }
11
- function persist(store, storageKey, session = false) {
12
- let inited = false;
13
- function read(state) {
14
- let storage = getStorage(session);
15
- let rawState = null;
16
- if (storage) {
17
- try {
18
- rawState = storage.getItem(storageKey);
19
- if (rawState !== null) store.setState(JSON.parse(rawState));
20
- } catch {
21
- }
22
- }
23
- if (!inited) {
24
- inited = true;
25
- if (rawState === null) write(state);
26
- }
27
- }
28
- function write(state) {
29
- let storage = getStorage(session);
30
- if (inited && storage) {
31
- try {
32
- storage.setItem(storageKey, JSON.stringify(state));
33
- } catch {
34
- }
35
- }
36
- }
37
- store.on("sync", read);
38
- store.once("effect", read);
39
- store.on("update", write);
40
- return store;
6
+ // node_modules/@t8/store/src/isPersistentStore.ts
7
+ function isPersistentStore(x) {
8
+ return isStore(x) && "sync" in x;
41
9
  }
42
10
 
43
11
  // node_modules/@t8/store/src/Store.ts
44
12
  var Store = class {
45
13
  state;
46
- callbacks = {};
14
+ callbacks = /* @__PURE__ */ new Set();
47
15
  revision = -1;
48
16
  constructor(data) {
49
17
  this.state = data;
50
18
  }
51
- on(event, callback) {
52
- (this.callbacks[event] ??= /* @__PURE__ */ new Set()).add(callback);
19
+ onUpdate(callback) {
20
+ this.callbacks.add(callback);
53
21
  return () => {
54
- this.off(event, callback);
55
- };
56
- }
57
- off(event, callback) {
58
- this.callbacks[event]?.delete(callback);
59
- }
60
- once(event, callback) {
61
- let oneTimeCallback = (nextState, prevState) => {
62
- this.off(event, oneTimeCallback);
63
- callback(nextState, prevState);
22
+ this.callbacks.delete(callback);
64
23
  };
65
- return this.on(event, oneTimeCallback);
66
- }
67
- emit(event, nextState, prevState) {
68
- let eventCallbacks = this.callbacks[event];
69
- if (eventCallbacks) {
70
- for (let callback of eventCallbacks)
71
- callback(nextState ?? this.state, prevState ?? this.state);
72
- }
73
24
  }
74
25
  getState() {
75
26
  return this.state;
@@ -79,7 +30,91 @@ var Store = class {
79
30
  let nextState = update instanceof Function ? update(this.state) : update;
80
31
  this.state = nextState;
81
32
  this.revision = Math.random();
82
- this.emit("update", nextState, prevState);
33
+ for (let callback of this.callbacks) callback(nextState, prevState);
34
+ }
35
+ };
36
+
37
+ // node_modules/@t8/store/src/PersistentStore.ts
38
+ function getStorage(session = false) {
39
+ if (typeof window === "undefined") return;
40
+ return session ? sessionStorage : localStorage;
41
+ }
42
+ var PersistentStore = class extends Store {
43
+ storageKey;
44
+ options;
45
+ synced = false;
46
+ /**
47
+ * Creates an instance of the container for data persistent across page
48
+ * reloads.
49
+ *
50
+ * The store data is saved to and restored from the given `storageKey`
51
+ * either of `localStorage` (by default) or `sessionStorage` (if `options.session`
52
+ * is set to `true`). Interaction with the browser storage is skipped in
53
+ * non-browser environments.
54
+ *
55
+ * @example
56
+ * ```js
57
+ * let counterStore = new PersistentStore(0, "counter");
58
+ * ```
59
+ *
60
+ * The way data gets saved to and restored from a browser storage entry
61
+ * (including filtering out certain data or otherwise rearranging the
62
+ * saved data) can be overridden by setting `options.serialize` and
63
+ * `options.deserialize`. By default, they are `JSON.stringify()` and
64
+ * `JSON.parse()`.
65
+ */
66
+ constructor(data, storageKey, options) {
67
+ super(data);
68
+ this.storageKey = storageKey;
69
+ this.options = {
70
+ session: false,
71
+ serialize: (data2) => JSON.stringify(data2),
72
+ deserialize: (content) => JSON.parse(content),
73
+ ...options
74
+ };
75
+ this.onUpdate(() => {
76
+ if (this.synced) this.save();
77
+ });
78
+ }
79
+ /**
80
+ * Saves the store state value to the browser storage.
81
+ */
82
+ save() {
83
+ let storage = getStorage(this.options?.session);
84
+ let serialize = this.options?.serialize;
85
+ if (this.synced && storage && typeof serialize === "function") {
86
+ try {
87
+ storage.setItem(this.storageKey, serialize(this.state));
88
+ } catch {
89
+ }
90
+ }
91
+ }
92
+ /**
93
+ * Signals the store to read the state value from the browser storage.
94
+ */
95
+ sync() {
96
+ let storage = getStorage(this.options?.session);
97
+ let deserialize = this.options?.deserialize;
98
+ let serializedState = null;
99
+ if (storage && typeof deserialize === "function") {
100
+ try {
101
+ serializedState = storage.getItem(this.storageKey);
102
+ if (serializedState !== null)
103
+ this.setState(deserialize(serializedState, this.state));
104
+ } catch {
105
+ }
106
+ }
107
+ if (!this.synced) {
108
+ this.synced = true;
109
+ if (serializedState === null) this.save();
110
+ }
111
+ }
112
+ /**
113
+ * Signals the store to read the state value from the browser storage once,
114
+ * disregarding subsequest `syncOnce()` calls.
115
+ */
116
+ syncOnce() {
117
+ if (!this.synced) this.sync();
83
118
  }
84
119
  };
85
120
 
@@ -92,9 +127,9 @@ function useStore(store, shouldUpdate = true) {
92
127
  let setState = useMemo(() => store.setState.bind(store), [store]);
93
128
  let initialStoreRevision = useRef(store.revision);
94
129
  useEffect(() => {
95
- store.emit("effect");
130
+ if (isPersistentStore(store)) store.syncOnce();
96
131
  if (!shouldUpdate) return;
97
- let unsubscribe = store.on("update", (nextState, prevState) => {
132
+ let unsubscribe = store.onUpdate((nextState, prevState) => {
98
133
  if (typeof shouldUpdate !== "function" || shouldUpdate(nextState, prevState))
99
134
  setRevision(Math.random());
100
135
  });
@@ -108,8 +143,9 @@ function useStore(store, shouldUpdate = true) {
108
143
  return [state, setState];
109
144
  }
110
145
  export {
146
+ PersistentStore,
111
147
  Store,
148
+ isPersistentStore,
112
149
  isStore,
113
- persist,
114
150
  useStore
115
151
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t8/react-store",
3
- "version": "1.1.3",
3
+ "version": "1.2.1",
4
4
  "description": "Small React app state management lib aligned with React's state pattern, condensed to the essentials",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -45,6 +45,6 @@
45
45
  "typescript": "^5.9.3"
46
46
  },
47
47
  "dependencies": {
48
- "@t8/store": "^1.2.3"
48
+ "@t8/store": "^1.3.3"
49
49
  }
50
50
  }
package/src/useStore.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { isStore, type Store } from "@t8/store";
1
+ import { isPersistentStore, isStore, type Store } from "@t8/store";
2
2
  import { useEffect, useMemo, useRef, useState } from "react";
3
3
 
4
4
  export type SetStoreState<T> = Store<T>["setState"];
@@ -45,14 +45,11 @@ export function useStore<T>(
45
45
  let initialStoreRevision = useRef(store.revision);
46
46
 
47
47
  useEffect(() => {
48
- // Use case: a one-time subscription to this event allows to
49
- // initialize the store state on the client without causing a
50
- // hydration error.
51
- store.emit("effect");
48
+ if (isPersistentStore<T>(store)) store.syncOnce();
52
49
 
53
50
  if (!shouldUpdate) return;
54
51
 
55
- let unsubscribe = store.on("update", (nextState, prevState) => {
52
+ let unsubscribe = store.onUpdate((nextState, prevState) => {
56
53
  if (
57
54
  typeof shouldUpdate !== "function" ||
58
55
  shouldUpdate(nextState, prevState)