reactish-state 1.0.0-alpha.0 → 1.0.0-alpha.2
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
|
@@ -1,61 +1,65 @@
|
|
|
1
1
|
# Reactish-State
|
|
2
2
|
|
|
3
|
-
> Simple, decentralized(atomic) state management for React.
|
|
3
|
+
> Simple, decentralized (atomic) state management for React.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/reactish-state) [](https://bundlephobia.com/package/reactish-state)
|
|
4
6
|
|
|
5
7
|
## ✨Highlights✨
|
|
6
8
|
|
|
7
9
|
- Decentralized state management
|
|
8
|
-
-
|
|
9
|
-
- No need
|
|
10
|
+
- Unopinionated and easy-to-use API
|
|
11
|
+
- No need to wrap app in Context or prop drilling
|
|
10
12
|
- React components re-render only on changes
|
|
11
|
-
- Compatible with React 18 concurrent rendering
|
|
13
|
+
- Compatible with React 18/19 concurrent rendering
|
|
12
14
|
- Selectors are memoized by default
|
|
13
15
|
- Feature extensible with middleware or plugins
|
|
14
|
-
-
|
|
15
|
-
- Support Redux dev tools via middleware
|
|
16
|
-
- [
|
|
16
|
+
- State persistable to browser storage
|
|
17
|
+
- Support for Redux dev tools via middleware
|
|
18
|
+
- [Less than 1KB](https://bundlejs.com/?q=reactish-state&treeshake=%5B*%5D&config=%7B%22esbuild%22%3A%7B%22external%22%3A%5B%22react%22%5D%7D%7D): simple and small
|
|
17
19
|
|
|
18
20
|
## Install
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
```bash
|
|
23
|
+
npm install reactish-state
|
|
24
|
+
```
|
|
21
25
|
|
|
22
26
|
## Quick start
|
|
23
27
|
|
|
24
|
-
### We begin by creating some
|
|
28
|
+
### We begin by creating some state
|
|
25
29
|
|
|
26
30
|
```js
|
|
27
31
|
import { state } from "reactish-state";
|
|
28
32
|
|
|
29
|
-
// `state` can hold anything: primitives, arrays, objects
|
|
33
|
+
// `state` can hold anything: primitives, arrays, objects, etc.
|
|
30
34
|
const countState = state(0);
|
|
31
35
|
const todos = state([
|
|
32
36
|
{ task: "Shop groceries", completed: false },
|
|
33
37
|
{ task: "Clean the house", completed: true }
|
|
34
38
|
]);
|
|
35
39
|
|
|
36
|
-
// Update state
|
|
40
|
+
// Update the state
|
|
37
41
|
countState.set(10);
|
|
38
|
-
// Read from state
|
|
42
|
+
// Read from the state
|
|
39
43
|
console.log(countState.get()); // Print 10
|
|
40
44
|
```
|
|
41
45
|
|
|
42
|
-
### A state can also have actions bound to it
|
|
46
|
+
### A state can also have custom actions bound to it
|
|
43
47
|
|
|
44
48
|
```js
|
|
45
49
|
const countState = state(0, (set, get) => ({
|
|
46
|
-
// Set a new state
|
|
50
|
+
// Set a new state value
|
|
47
51
|
reset: () => set(0),
|
|
48
|
-
//
|
|
52
|
+
// Or use the functional update of `set`
|
|
49
53
|
increase: () => set((count) => count + 1),
|
|
50
54
|
// State can still be read using `get`
|
|
51
55
|
decrease: () => set(get() - 1)
|
|
52
56
|
}));
|
|
53
57
|
|
|
54
|
-
//
|
|
55
|
-
countState.
|
|
58
|
+
// Use the custom actions
|
|
59
|
+
countState.increase();
|
|
56
60
|
```
|
|
57
61
|
|
|
58
|
-
### `selector` can create derived
|
|
62
|
+
### `selector` can create derived state
|
|
59
63
|
|
|
60
64
|
```js
|
|
61
65
|
import { selector } from "reactish-state";
|
|
@@ -71,11 +75,11 @@ const tripleSelector = selector(
|
|
|
71
75
|
);
|
|
72
76
|
```
|
|
73
77
|
|
|
74
|
-
A selector will re-compute only when
|
|
78
|
+
A selector will re-compute only when one of the states it depends on has changed.
|
|
75
79
|
|
|
76
|
-
### Use the state and
|
|
80
|
+
### Use the state and selectors in your React components
|
|
77
81
|
|
|
78
|
-
You can read state and
|
|
82
|
+
You can read state and selectors for rendering with the `useSnapshot` hook, and write to state with `set` or actions. _Rule of thumb_: always read from `useSnapshot` in the render function; otherwise, use the `get` method of state or selector (in event handlers or even outside of React components).
|
|
79
83
|
|
|
80
84
|
```jsx
|
|
81
85
|
import { useSnapshot } from "reactish-state";
|
|
@@ -86,10 +90,11 @@ const Example = () => {
|
|
|
86
90
|
|
|
87
91
|
return (
|
|
88
92
|
<h1>
|
|
93
|
+
{/* The return values of `useSnapshot` are used for rendering */}
|
|
89
94
|
{count} {triple}
|
|
90
|
-
{/* Update state using the actions bound to it */}
|
|
91
|
-
<button onClick={() => countState.
|
|
92
|
-
{/* Or update state using the `set` method directly */}
|
|
95
|
+
{/* Update the state using the custom actions bound to it */}
|
|
96
|
+
<button onClick={() => countState.increase()}>Increase</button>
|
|
97
|
+
{/* Or update the state using the `set` method directly */}
|
|
93
98
|
<button onClick={() => countState.set((i) => i - 1)}>Decrease</button>
|
|
94
99
|
<button onClick={() => countState.set(0)}>Reset</button>
|
|
95
100
|
</h1>
|
|
@@ -97,19 +102,19 @@ const Example = () => {
|
|
|
97
102
|
};
|
|
98
103
|
```
|
|
99
104
|
|
|
100
|
-
The component will re-render when states or selectors
|
|
105
|
+
The component will re-render when states or selectors change. No provider or context is needed!
|
|
101
106
|
|
|
102
107
|
**[Try a sandbox demo!](https://codesandbox.io/p/sandbox/reactish-counter-z42qt7)**
|
|
103
108
|
|
|
104
109
|
## Why another state management library?
|
|
105
110
|
|
|
106
|
-
|
|
111
|
+
State management solutions in the React ecosystem have popularized two state models:
|
|
107
112
|
|
|
108
|
-
- **Centralized**: a single store that combines entire app
|
|
113
|
+
- **Centralized**: a single store that combines the entire app's state, with slices of the store connected to React components via selectors. Examples: React-Redux, Zustand.
|
|
109
114
|
|
|
110
|
-
- **Decentralized**:
|
|
115
|
+
- **Decentralized**: composed of many small (atomic) states that build state dependency trees using a bottom-up approach. React components only connect to the states they need. Examples: Recoil, Jotai.
|
|
111
116
|
|
|
112
|
-
This library adopts the decentralized state model, offering a _Recoil-like_ API
|
|
117
|
+
This library adopts the decentralized state model, offering a _Recoil-like_ API with a much smaller implementation (similar to Zustand). This makes it one of the smallest state management solutions, with a gzipped bundle size of less than 1KB.
|
|
113
118
|
|
|
114
119
|
| | State model | Bundle size |
|
|
115
120
|
| --- | --- | --- |
|
|
@@ -121,18 +126,18 @@ This library adopts the decentralized state model, offering a _Recoil-like_ API,
|
|
|
121
126
|
|
|
122
127
|
## Why decentralized state management?
|
|
123
128
|
|
|
124
|
-
Centralized state management
|
|
129
|
+
Centralized state management typically combines the entire app's state into a single store. To optimize rendering, selectors are used to subscribe React components to slices of the store. Taking the classic [Redux todo example](https://redux.js.org/introduction/examples#todos), the store has the following shape:
|
|
125
130
|
|
|
126
131
|
```js
|
|
127
132
|
{
|
|
128
133
|
visibilityFilter: "ALL", // ALL, ACTIVE, COMPLETED
|
|
129
|
-
todos: [{ task: "Shop groceries", completed: false } /* ...more items */]
|
|
134
|
+
todos: [{ task: "Shop groceries", completed: false } /* ...and more items */]
|
|
130
135
|
}
|
|
131
136
|
```
|
|
132
137
|
|
|
133
138
|
We have a `<Filter/>` component that connects to the store with a selector `(state) => state.visibilityFilter`.
|
|
134
139
|
|
|
135
|
-
When any action updates the `todos` slice, the selector in the `<Filter/>` component needs to re-run to determine if a re-
|
|
140
|
+
When any action updates the `todos` slice, the selector in the `<Filter/>` component needs to re-run to determine if a re-render is required. This is not optimal, as the `<Filter/>` component should not be affected when todos are added, removed, or updated.
|
|
136
141
|
|
|
137
142
|
In contrast, decentralized state management may approach the same problem with two separate states:
|
|
138
143
|
|
|
@@ -140,24 +145,24 @@ In contrast, decentralized state management may approach the same problem with t
|
|
|
140
145
|
const visibilityFilter = state("ALL"); // ALL, ACTIVE, COMPLETED
|
|
141
146
|
const todos = state([
|
|
142
147
|
{ task: "Shop groceries", completed: false }
|
|
143
|
-
/* ...more items */
|
|
148
|
+
/* ...and more items */
|
|
144
149
|
]);
|
|
145
150
|
```
|
|
146
151
|
|
|
147
|
-
An update
|
|
152
|
+
An update to `todos`, which is localized and isolated from other states, does not affect components connected to `visibilityFilter` and vice versa.
|
|
148
153
|
|
|
149
|
-
|
|
154
|
+
While the difference might seem insignificant, imagine that every small state update could cause every selector in every component across the entire app to re-run. This suggests that the decentralized state model scales better for large apps. Additionally, benefits like code-splitting are easier to implement with this state model.
|
|
150
155
|
|
|
151
|
-
## Why this over Zustand?
|
|
156
|
+
## Why choose this over Zustand?
|
|
152
157
|
|
|
153
|
-
- State updates localized and isolated from other irrelevant states.
|
|
154
|
-
- No potential naming conflicts among states/actions within
|
|
158
|
+
- State updates are localized and isolated from other irrelevant states.
|
|
159
|
+
- No potential naming conflicts among states/actions within a large store.
|
|
155
160
|
- No need to use a React Hook to extract actions from the store.
|
|
156
|
-
- Actions
|
|
161
|
+
- Actions are external to React, eliminating the need to add them to the `useCallback/useEffect` dep array.
|
|
157
162
|
|
|
158
163
|
# Recipes
|
|
159
164
|
|
|
160
|
-
##
|
|
165
|
+
## State should be updated immutably
|
|
161
166
|
|
|
162
167
|
```js
|
|
163
168
|
import { state } from "reactish-state";
|
|
@@ -185,7 +190,7 @@ Or, simply use the [immer middleware](#immer-middleware).
|
|
|
185
190
|
|
|
186
191
|
## Selectors are memoized
|
|
187
192
|
|
|
188
|
-
Selector has an API
|
|
193
|
+
Selector has an API similar to the [reselect](https://github.com/reduxjs/reselect#readme) package. You pass in one or more 'input' states or selectors, along with an 'output' selector function that receives the extracted values and returns a derived value. The return value is memoized, ensuring that React components won’t re-render even if a non-primitive value is returned.
|
|
189
194
|
|
|
190
195
|
```js
|
|
191
196
|
import { selector } from "reactish-state";
|
|
@@ -209,9 +214,11 @@ const todoStats = selector(
|
|
|
209
214
|
);
|
|
210
215
|
```
|
|
211
216
|
|
|
217
|
+
The only difference between state and selector is that selectors are read-only and don’t have a `set` method.
|
|
218
|
+
|
|
212
219
|
## Async state updates
|
|
213
220
|
|
|
214
|
-
Just call `set` when
|
|
221
|
+
Just call `set` when your data is ready:
|
|
215
222
|
|
|
216
223
|
```js
|
|
217
224
|
const todosState = state([]);
|
|
@@ -226,16 +233,19 @@ You can also create async actions bound to a state:
|
|
|
226
233
|
|
|
227
234
|
```js
|
|
228
235
|
const todosState = state([], (set) => ({
|
|
229
|
-
|
|
230
|
-
const response = await fetch(url);
|
|
236
|
+
fetchData: async () => {
|
|
237
|
+
const response = await fetch(/* some url */);
|
|
231
238
|
set(await response.json());
|
|
232
239
|
}
|
|
233
240
|
}));
|
|
241
|
+
|
|
242
|
+
// Use the async action
|
|
243
|
+
await todosState.fetchData();
|
|
234
244
|
```
|
|
235
245
|
|
|
236
|
-
## Accessing other state or
|
|
246
|
+
## Accessing other state or selectors inside actions
|
|
237
247
|
|
|
238
|
-
You might not need it, but nothing
|
|
248
|
+
You might not need it, but nothing prevents you from reading or writing to other state inside an action.
|
|
239
249
|
|
|
240
250
|
```js
|
|
241
251
|
const inputState = state("New item");
|
|
@@ -250,7 +260,7 @@ const todosState = state(
|
|
|
250
260
|
);
|
|
251
261
|
```
|
|
252
262
|
|
|
253
|
-
## Interacting with state or
|
|
263
|
+
## Interacting with state or selectors outside React
|
|
254
264
|
|
|
255
265
|
```js
|
|
256
266
|
const countState = state(0);
|
|
@@ -260,27 +270,25 @@ const tripleSelector = selector(countState, (count) => count * 3);
|
|
|
260
270
|
const count = countState.get();
|
|
261
271
|
const triple = tripleSelector.get();
|
|
262
272
|
|
|
263
|
-
// Listen
|
|
273
|
+
// Listen for updates
|
|
264
274
|
const unsub1 = countState.subscribe(() => console.log(countState.get()));
|
|
265
275
|
const unsub2 = tripleSelector.subscribe(() =>
|
|
266
276
|
console.log(tripleSelector.get())
|
|
267
277
|
);
|
|
268
278
|
|
|
269
|
-
//
|
|
279
|
+
// Updating `countState` will trigger both listeners
|
|
270
280
|
countState.set(10);
|
|
271
281
|
|
|
272
|
-
// Unsubscribe listeners
|
|
282
|
+
// Unsubscribe from listeners
|
|
273
283
|
unsub1();
|
|
274
284
|
unsub2();
|
|
275
285
|
```
|
|
276
286
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
## Destructuring actions for easier reference
|
|
287
|
+
## Destructuring actions for easier access
|
|
280
288
|
|
|
281
|
-
The `set` or actions of a state don't rely on `this` to work,
|
|
289
|
+
The `set` or actions of a state don't rely on `this` to work, so you can destructure them for easier reference.
|
|
282
290
|
|
|
283
|
-
_TIP_:
|
|
291
|
+
_TIP_: Destructure the actions outside of React components to avoid adding them to the `useCallback/useEffect` dependency array.
|
|
284
292
|
|
|
285
293
|
```jsx
|
|
286
294
|
import { state, useSnapshot } from "reactish-state";
|
|
@@ -289,7 +297,7 @@ const countState = state(0, (set) => ({
|
|
|
289
297
|
increase: () => set((count) => count + 1),
|
|
290
298
|
reset: () => set(0)
|
|
291
299
|
}));
|
|
292
|
-
const { increase, reset } = countState
|
|
300
|
+
const { increase, reset } = countState;
|
|
293
301
|
|
|
294
302
|
const Example = () => {
|
|
295
303
|
const count = useSnapshot(countState);
|
|
@@ -303,9 +311,9 @@ const Example = () => {
|
|
|
303
311
|
};
|
|
304
312
|
```
|
|
305
313
|
|
|
306
|
-
## Selector that depends on props or local
|
|
314
|
+
## Selector that depends on props or local state
|
|
307
315
|
|
|
308
|
-
The `selector` function allows us to create reusable derived states outside React components. In contrast, component-scoped derived states
|
|
316
|
+
The `selector` function allows us to create reusable derived states outside of React components. In contrast, component-scoped derived states that depend on props or local state can be created using the `useSelector` hook.
|
|
309
317
|
|
|
310
318
|
```jsx
|
|
311
319
|
import { state, useSelector } from "reactish-state";
|
|
@@ -329,21 +337,21 @@ const FilteredTodoList = ({ filter = "ALL" }) => {
|
|
|
329
337
|
],
|
|
330
338
|
[filter]
|
|
331
339
|
);
|
|
332
|
-
// Render filtered todos...
|
|
340
|
+
// Render the filtered todos...
|
|
333
341
|
};
|
|
334
342
|
```
|
|
335
343
|
|
|
336
|
-
The second parameter of `useSelector` is a dependency array (similar to React's `useMemo` hook),
|
|
344
|
+
The second parameter of `useSelector` is a dependency array (similar to React's `useMemo` hook), where you can specify which props or local state the selector depends on. In the example above, the `FilteredTodoList` component will re-render only if the global `todosState` or the local `filter` prop is updated.
|
|
337
345
|
|
|
338
346
|
### Linting the dependency array of useSelector
|
|
339
347
|
|
|
340
|
-
You can take advantage of the [eslint-plugin-react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks) package to lint the dependency array of `useSelector`.
|
|
348
|
+
You can take advantage of the [eslint-plugin-react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks) package to lint the dependency array of `useSelector`. Simply add the following configuration to your ESLint config file:
|
|
341
349
|
|
|
342
350
|
```json
|
|
343
351
|
{
|
|
344
352
|
"rules": {
|
|
345
353
|
"react-hooks/exhaustive-deps": [
|
|
346
|
-
"
|
|
354
|
+
"error",
|
|
347
355
|
{
|
|
348
356
|
"additionalHooks": "useSelector"
|
|
349
357
|
}
|
|
@@ -368,7 +376,7 @@ const countState = state(0, (set) => ({
|
|
|
368
376
|
dispatch: (action) => set((state) => reducer(state, action), action)
|
|
369
377
|
}));
|
|
370
378
|
|
|
371
|
-
const { dispatch } = countState
|
|
379
|
+
const { dispatch } = countState;
|
|
372
380
|
dispatch({ type: "INCREASE", by: 10 });
|
|
373
381
|
dispatch({ type: "DECREASE", by: 7 });
|
|
374
382
|
console.log(countState.get()); // Print 3
|
|
@@ -376,7 +384,7 @@ console.log(countState.get()); // Print 3
|
|
|
376
384
|
|
|
377
385
|
## Middleware
|
|
378
386
|
|
|
379
|
-
You can enhance the
|
|
387
|
+
You can enhance the functionality of state with middleware. Instead of using the `state` export, use the `createState` export from the library. Middleware is a function that receives `set`, `get`, and `subscribe`, and should return a new set function.
|
|
380
388
|
|
|
381
389
|
```js
|
|
382
390
|
import { createState } from "reactish-state";
|
|
@@ -386,35 +394,35 @@ const state = createState({
|
|
|
386
394
|
({ set, get }) =>
|
|
387
395
|
(...args) => {
|
|
388
396
|
set(...args);
|
|
389
|
-
// Log state every time after calling `set`
|
|
397
|
+
// Log the state every time after calling `set`
|
|
390
398
|
console.log("New state", get());
|
|
391
399
|
}
|
|
392
400
|
});
|
|
393
401
|
|
|
394
|
-
// Now the `state` function has wired up
|
|
402
|
+
// Now the `state` function has middleware wired up
|
|
395
403
|
const countState = state(0, (set) => ({
|
|
396
404
|
increase: () => set((count) => count + 1)
|
|
397
405
|
}));
|
|
398
406
|
|
|
399
407
|
countState.set(99); // Print "New state 99"
|
|
400
|
-
countState.
|
|
408
|
+
countState.increase(); // Print "New state 100"
|
|
401
409
|
|
|
402
410
|
// The same `state` function can be reused,
|
|
403
|
-
//
|
|
411
|
+
// so you don't need to set up the middleware again
|
|
404
412
|
const filterState = state("ALL");
|
|
405
413
|
filterState.set("COMPLETED"); // Print "New state 'COMPLETED'"
|
|
406
414
|
```
|
|
407
415
|
|
|
408
416
|
## Persist middleware
|
|
409
417
|
|
|
410
|
-
You can save state
|
|
418
|
+
You can save the state to browser storage using the `persist` middleware.
|
|
411
419
|
|
|
412
420
|
```js
|
|
413
421
|
import { createState } from "reactish-state";
|
|
414
422
|
import { persist } from "reactish-state/middleware";
|
|
415
423
|
|
|
416
424
|
// Create the persist middleware,
|
|
417
|
-
//
|
|
425
|
+
// optionally provide a `prefix` to prepend to the keys in storage
|
|
418
426
|
const persistMiddleware = persist({ prefix: "myApp-" });
|
|
419
427
|
const state = createState({ middleware: persistMiddleware });
|
|
420
428
|
|
|
@@ -423,20 +431,20 @@ const countState = state(
|
|
|
423
431
|
(set) => ({
|
|
424
432
|
increase: () => set((count) => count + 1)
|
|
425
433
|
}),
|
|
426
|
-
{ key: "count" } // In the third parameter,
|
|
434
|
+
{ key: "count" } // In the third parameter, assign each state a unique key
|
|
427
435
|
);
|
|
428
436
|
const filterState = state("ALL", null, { key: "filter" });
|
|
429
437
|
|
|
430
438
|
// Hydrate all the states created with this middleware from storage
|
|
431
439
|
useEffect(() => {
|
|
432
|
-
// Call `hydrate` in
|
|
440
|
+
// Call `hydrate` in a `useEffect` to avoid client-side mismatch,
|
|
433
441
|
// if React components are also server-rendered
|
|
434
442
|
persistMiddleware.hydrate();
|
|
435
443
|
}, []);
|
|
436
|
-
// You can add the `useEffect` once
|
|
444
|
+
// You can add the `useEffect` once in your root component
|
|
437
445
|
```
|
|
438
446
|
|
|
439
|
-
By default `localStorage` is used to persist states. You can
|
|
447
|
+
By default, `localStorage` is used to persist states. You can switch to `sessionStorage` or other implementations by using the `getStorage` option.
|
|
440
448
|
|
|
441
449
|
```js
|
|
442
450
|
const persistMiddleware = persist({ getStorage: () => sessionStorage });
|
|
@@ -444,7 +452,7 @@ const persistMiddleware = persist({ getStorage: () => sessionStorage });
|
|
|
444
452
|
|
|
445
453
|
## Immer middleware
|
|
446
454
|
|
|
447
|
-
You can
|
|
455
|
+
You can update state mutably using the `immer` middleware.
|
|
448
456
|
|
|
449
457
|
```js
|
|
450
458
|
import { createState } from "reactish-state";
|
|
@@ -457,8 +465,8 @@ const todos = state([], (set) => ({
|
|
|
457
465
|
add: (task) =>
|
|
458
466
|
set((todos) => {
|
|
459
467
|
todos.push({ id: todoId++, task, completed: false });
|
|
460
|
-
//
|
|
461
|
-
|
|
468
|
+
// Return the draft state for correct typing in TypeScript
|
|
469
|
+
return todos;
|
|
462
470
|
}),
|
|
463
471
|
|
|
464
472
|
toggle: (id) =>
|
|
@@ -468,14 +476,14 @@ const todos = state([], (set) => ({
|
|
|
468
476
|
})
|
|
469
477
|
}));
|
|
470
478
|
|
|
471
|
-
//
|
|
472
|
-
todos.
|
|
473
|
-
todos.
|
|
479
|
+
// Use the actions
|
|
480
|
+
todos.add("Shop groceries");
|
|
481
|
+
todos.toggle(1);
|
|
474
482
|
```
|
|
475
483
|
|
|
476
484
|
## Redux devtools middleware
|
|
477
485
|
|
|
478
|
-
Individual
|
|
486
|
+
This middleware provides integration with the Redux DevTools browser extension. Individual states are combined into a single object in Redux DevTools for easy inspection.
|
|
479
487
|
|
|
480
488
|
```js
|
|
481
489
|
import { createState } from "reactish-state";
|
|
@@ -491,7 +499,7 @@ const todos = state(
|
|
|
491
499
|
(todos) => {
|
|
492
500
|
/* Add todo */
|
|
493
501
|
},
|
|
494
|
-
// Log action type in the second parameter of `set`
|
|
502
|
+
// Log the action type in the second parameter of `set`
|
|
495
503
|
"todo/add"
|
|
496
504
|
),
|
|
497
505
|
toggle: (id) =>
|
|
@@ -499,21 +507,21 @@ const todos = state(
|
|
|
499
507
|
(todos) => {
|
|
500
508
|
/* Toggle todo */
|
|
501
509
|
},
|
|
502
|
-
// You can also log action type along with its payload
|
|
510
|
+
// You can also log the action type along with its payload
|
|
503
511
|
{ type: "todo/toggle", id }
|
|
504
512
|
)
|
|
505
513
|
}),
|
|
506
|
-
// Similar to the persist middleware,
|
|
514
|
+
// Similar to the persist middleware, assign each state a unique key
|
|
507
515
|
{ key: "todos" }
|
|
508
516
|
);
|
|
509
517
|
|
|
510
|
-
// `todos` and `filter` will be combined into
|
|
518
|
+
// `todos` and `filter` will be combined into a single object in Redux DevTools
|
|
511
519
|
const filter = state("ALL", null, { key: "filter" });
|
|
512
520
|
```
|
|
513
521
|
|
|
514
522
|
## Using multiple middleware
|
|
515
523
|
|
|
516
|
-
Middleware is
|
|
524
|
+
Middleware is chainable. You can use the `applyMiddleware` utility to chain multiple middleware and pass the result to `createState`.
|
|
517
525
|
|
|
518
526
|
```js
|
|
519
527
|
import { applyMiddleware } from "reactish-state/middleware";
|
|
@@ -535,11 +543,11 @@ const visibilityFilter = persistState("ALL"); // Will be persisted
|
|
|
535
543
|
const todos = immerState([]); // Can be mutated
|
|
536
544
|
```
|
|
537
545
|
|
|
538
|
-
|
|
546
|
+
This also eliminates the need to implement a whitelist or blacklist in the persist middleware.
|
|
539
547
|
|
|
540
548
|
## Plugins
|
|
541
549
|
|
|
542
|
-
While
|
|
550
|
+
While middleware enhances state, plugins allow you to hook into selectors. The key difference is that plugins don’t return a `set` function, as selectors are read-only. Similarly, you use the `createSelector` export from the library instead of `selector`.
|
|
543
551
|
|
|
544
552
|
```js
|
|
545
553
|
import { state, createSelector } from "reactish-state";
|
|
@@ -547,8 +555,8 @@ import { state, createSelector } from "reactish-state";
|
|
|
547
555
|
const selector = createSelector({
|
|
548
556
|
plugin: ({ get, subscribe }, config) => {
|
|
549
557
|
subscribe(() => {
|
|
550
|
-
// Log selector value every time
|
|
551
|
-
// `config` can hold contextual data from
|
|
558
|
+
// Log the selector value every time it changes
|
|
559
|
+
// `config` can hold contextual data from the selector
|
|
552
560
|
console.log(`${config?.key} selector:`, get());
|
|
553
561
|
});
|
|
554
562
|
}
|
|
@@ -558,7 +566,7 @@ const countState = state(0);
|
|
|
558
566
|
const doubleSelector = selector(
|
|
559
567
|
countState,
|
|
560
568
|
(count) => count * 2,
|
|
561
|
-
// Provide contextual data in the last parameter to
|
|
569
|
+
// Provide contextual data in the last parameter to identify the selector
|
|
562
570
|
{
|
|
563
571
|
key: "double"
|
|
564
572
|
}
|
|
@@ -567,23 +575,81 @@ const squareSelector = selector(countState, (count) => count * count, {
|
|
|
567
575
|
key: "square"
|
|
568
576
|
});
|
|
569
577
|
|
|
570
|
-
countState.set(5); //
|
|
578
|
+
countState.set(5); // Logs - double selector: 10, square selector: 25
|
|
571
579
|
```
|
|
572
580
|
|
|
573
581
|
Likewise, there is an `applyPlugin` function for applying multiple plugins.
|
|
574
582
|
|
|
575
583
|
## Redux devtools plugin
|
|
576
584
|
|
|
577
|
-
Individual
|
|
585
|
+
Individual selectors are combined into a single object in Redux DevTools for easy inspection.
|
|
578
586
|
|
|
579
587
|
```js
|
|
580
588
|
import { createSelector } from "reactish-state";
|
|
581
589
|
import { reduxDevtools } from "reactish-state/plugin";
|
|
582
590
|
|
|
583
591
|
const selector = createSelector({ plugin: reduxDevtools() });
|
|
584
|
-
// Then use the `selector` as
|
|
592
|
+
// Then use the `selector` as usual...
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
# TypeScript usage
|
|
596
|
+
|
|
597
|
+
The API relies on type inference to correctly infer the types for both the value and actions of the state. There are two scenarios:
|
|
598
|
+
|
|
599
|
+
## I. The type of state can be inferred from its initial value
|
|
600
|
+
|
|
601
|
+
In this case, the usage in TypeScript should be identical to JavaScript. You don't need to make any specific effort regarding typing. This is true when the state holds simple or primitive values.
|
|
602
|
+
|
|
603
|
+
## II. The type of state cannot be inferred from its initial value
|
|
604
|
+
|
|
605
|
+
In this case, you have three options:
|
|
606
|
+
|
|
607
|
+
### 1. Use a type assertion to specify a more specific type for the initial value:
|
|
608
|
+
|
|
609
|
+
```ts
|
|
610
|
+
const myTodos = state([] as string[], (set) => ({
|
|
611
|
+
add: (newTodo: string) => set((todos) => [...todos, newTodo])
|
|
612
|
+
}));
|
|
585
613
|
```
|
|
586
614
|
|
|
615
|
+
This is the simplest approach since the types for custom actions will be automatically inferred.
|
|
616
|
+
|
|
617
|
+
### 2. Declare the initial value separately with a specific type:
|
|
618
|
+
|
|
619
|
+
```ts
|
|
620
|
+
const initialValue: string[] = [];
|
|
621
|
+
const myTodos = state(initialValue, (set) => ({
|
|
622
|
+
add: (newTodo: string) => set((todos) => [...todos, newTodo])
|
|
623
|
+
}));
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
This is basically very similar to the first method, except you need to write an additional line of code. The types for actions will be automatically inferred.
|
|
627
|
+
|
|
628
|
+
### 3. Specify type parameters explicitly:
|
|
629
|
+
|
|
630
|
+
```ts
|
|
631
|
+
const myTodos = state<string[], { add: (newTodo: string) => void }>(
|
|
632
|
+
[],
|
|
633
|
+
(set) => ({
|
|
634
|
+
add: (newTodo) => set((todos) => [...todos, newTodo])
|
|
635
|
+
})
|
|
636
|
+
);
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
However, if you choose this method, you need to specify the types for both the state value and actions.
|
|
640
|
+
|
|
641
|
+
# React 16/17 setup
|
|
642
|
+
|
|
643
|
+
When using this library with React 16/17, you must set up a shim since it doesn't include a native [useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore). We don't set up the shim by default to minimize the bundle size for React 18/19 users.
|
|
644
|
+
|
|
645
|
+
```js
|
|
646
|
+
import { setReactShim } from "reactish-state";
|
|
647
|
+
import { reactShim } from "reactish-state/shim";
|
|
648
|
+
setReactShim(reactShim);
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
You only need to set it up once after your app launches, outside of React code. DO NOT call `setReactShim` within any React components.
|
|
652
|
+
|
|
587
653
|
# Examples
|
|
588
654
|
|
|
589
655
|
- Counter – [sandbox](https://codesandbox.io/p/sandbox/reactish-counter-z42qt7) | [source](https://github.com/szhsin/reactish-state/tree/master/examples/examples/counter)
|
|
@@ -7,7 +7,7 @@ const useSnapshot = ({
|
|
|
7
7
|
get
|
|
8
8
|
}) => {
|
|
9
9
|
if (process.env.NODE_ENV !== 'production' && !shim.useSyncExternalStore) {
|
|
10
|
-
throw new Error('[reactish-state] Shim setup is required for React 16/17.');
|
|
10
|
+
throw new Error('[reactish-state] Shim setup is required for React 16/17. See: https://github.com/szhsin/reactish-state/tree/master?tab=readme-ov-file#react-1617-setup');
|
|
11
11
|
}
|
|
12
12
|
return shim.useSyncExternalStore(subscribe, get, get);
|
|
13
13
|
};
|
|
@@ -5,7 +5,7 @@ const useSnapshot = ({
|
|
|
5
5
|
get
|
|
6
6
|
}) => {
|
|
7
7
|
if (process.env.NODE_ENV !== 'production' && !useSyncExternalStore) {
|
|
8
|
-
throw new Error('[reactish-state] Shim setup is required for React 16/17.');
|
|
8
|
+
throw new Error('[reactish-state] Shim setup is required for React 16/17. See: https://github.com/szhsin/reactish-state/tree/master?tab=readme-ov-file#react-1617-setup');
|
|
9
9
|
}
|
|
10
10
|
return useSyncExternalStore(subscribe, get, get);
|
|
11
11
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reactish-state",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.2",
|
|
4
4
|
"description": "Simple, decentralized state management for React.",
|
|
5
5
|
"author": "Zheng Song",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"dist/",
|
|
18
18
|
"types/",
|
|
19
19
|
"middleware/",
|
|
20
|
-
"plugin/"
|
|
20
|
+
"plugin/",
|
|
21
|
+
"shim/"
|
|
21
22
|
],
|
|
22
23
|
"keywords": [
|
|
23
24
|
"react",
|
package/shim/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../types/shim';
|
package/types/vanilla/state.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { Reactish, Setter, Config, Middleware } from '../common';
|
|
2
2
|
type ActionCreator<T, A> = ((set: Setter<T>, get: () => T) => A) | null | undefined;
|
|
3
|
-
|
|
3
|
+
type VanillaState<T> = Reactish<T> & {
|
|
4
4
|
set: Setter<T>;
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
};
|
|
6
|
+
type State<T, A> = Omit<A, keyof VanillaState<T>> & VanillaState<T>;
|
|
7
7
|
declare const createState: ({ middleware }?: {
|
|
8
8
|
middleware?: Middleware;
|
|
9
|
-
}) => <T, A>(initialValue: T, actionCreator?: ActionCreator<T, A>, config?: Config) => State<T, A
|
|
10
|
-
declare const state: <T, A>(initialValue: T, actionCreator?: ActionCreator<T, A>, config?: Config) => State<T, A
|
|
9
|
+
}) => <T, A>(initialValue: T, actionCreator?: ActionCreator<T, A>, config?: Config) => State<T, A>;
|
|
10
|
+
declare const state: <T, A>(initialValue: T, actionCreator?: ActionCreator<T, A>, config?: Config) => State<T, A>;
|
|
11
11
|
export type { State, ActionCreator };
|
|
12
12
|
export { state, createState };
|