reactish-state 0.10.2 → 0.10.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 +542 -0
- package/package.json +29 -21
- package/types/common.d.ts +4 -4
- package/types/middleware/persist.d.ts +1 -1
- package/types/middleware/reduxDevtools.d.ts +1 -1
- package/types/plugin/reduxDevtools.d.ts +1 -1
- package/types/vanilla/selector.d.ts +3 -3
- package/types/vanilla/state.d.ts +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
# Reactish-State
|
|
2
|
+
|
|
3
|
+
> Simple, decentralized state management for React.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
`npm install reactish-state` or `yarn add reactish-state`
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
### We begin by creating some states
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import { state } from "reactish-state";
|
|
15
|
+
|
|
16
|
+
// `state` can hold anything: primitives, arrays, objects...
|
|
17
|
+
const countState = state(0);
|
|
18
|
+
const todos = state([
|
|
19
|
+
{ task: "Shop groceries", completed: false },
|
|
20
|
+
{ task: "Clean the house", completed: true }
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
// Update state
|
|
24
|
+
countState.set(10);
|
|
25
|
+
// Read from state
|
|
26
|
+
console.log(countState.get()); // Print 10
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### A state can also have actions bound to it
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
const countState = state(0, (set, get) => ({
|
|
33
|
+
// Set a new state
|
|
34
|
+
reset: () => set(0),
|
|
35
|
+
// or using the functional update of `set`
|
|
36
|
+
increase: () => set((count) => count + 1),
|
|
37
|
+
// State can still be read using `get`
|
|
38
|
+
decrease: () => set(get() - 1)
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
// Using the actions
|
|
42
|
+
countState.actions.increase();
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### `selector` can create derived states
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
import { selector } from "reactish-state";
|
|
49
|
+
|
|
50
|
+
// Derive from another state
|
|
51
|
+
const doubleSelector = selector(countState, (count) => count * 2);
|
|
52
|
+
|
|
53
|
+
// Can also derive from both states and selectors
|
|
54
|
+
const tripleSelector = selector(
|
|
55
|
+
countState,
|
|
56
|
+
doubleSelector,
|
|
57
|
+
(count, double) => count + double
|
|
58
|
+
);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
A selector will re-compute only when any of the states it depends on have changed.
|
|
62
|
+
|
|
63
|
+
### Use the state and selector in your React components
|
|
64
|
+
|
|
65
|
+
You can read state and selector for rendering with the `useSnapshot` hook, and write to state with `set` or actions. _Rule of thumb_: always read from `useSnapshot` in render function, otherwise use the `get` method of state or selector.
|
|
66
|
+
|
|
67
|
+
```jsx
|
|
68
|
+
import { useSnapshot } from "reactish-state";
|
|
69
|
+
|
|
70
|
+
const Example = () => {
|
|
71
|
+
const count = useSnapshot(countState);
|
|
72
|
+
const triple = useSnapshot(tripleSelector);
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<h1>
|
|
76
|
+
{count} {triple}
|
|
77
|
+
{/* Update state using the actions bound to it */}
|
|
78
|
+
<button onClick={() => countState.actions.increase()}>Increase</button>
|
|
79
|
+
{/* Or update state using the `set` method directly */}
|
|
80
|
+
<button onClick={() => countState.set((i) => i - 1)}>Decrease</button>
|
|
81
|
+
<button onClick={() => countState.set(0)}>Reset</button>
|
|
82
|
+
</h1>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The component will re-render when states or selectors have changed. No provider or context are needed!
|
|
88
|
+
|
|
89
|
+
**[Try a sandbox demo!](https://codesandbox.io/s/reactish-counter-3let0o)**
|
|
90
|
+
|
|
91
|
+
## Why another state management library?
|
|
92
|
+
|
|
93
|
+
The state management solutions in the React ecosystem have popularized two state models:
|
|
94
|
+
|
|
95
|
+
- **Centralized**: a single store that combines entire app states together and slices of the store are connected to React components through selectors. Examples: react-redux, Zustand.
|
|
96
|
+
|
|
97
|
+
- **Decentralized**: consisting of many small states which can build up state dependency trees using a bottom-up approach. React components only need to connect with the states that they use. Examples: Recoil, Jotai.
|
|
98
|
+
|
|
99
|
+
This library adopts the decentralized state model, offering a _Recoil-like_ API, but with a much simpler and smaller implementation(similar to Zustand), which makes it the one of the smallest state management solutions with gzipped bundle size less than 1KB.
|
|
100
|
+
|
|
101
|
+
| | State model | Bundle size |
|
|
102
|
+
| --- | --- | --- |
|
|
103
|
+
| Reactish-State | decentralized | [](https://bundlephobia.com/package/reactish-state) |
|
|
104
|
+
| Recoil | decentralized | [](https://bundlephobia.com/package/recoil) |
|
|
105
|
+
| Jotai | decentralized | [](https://bundlephobia.com/package/jotai) |
|
|
106
|
+
| React-Redux | centralized | [](https://bundlephobia.com/package/react-redux) |
|
|
107
|
+
| Zustand | centralized | [](https://bundlephobia.com/package/zustand) |
|
|
108
|
+
|
|
109
|
+
## Why decentralized state management?
|
|
110
|
+
|
|
111
|
+
Centralized state management usually combines the entire app states into a single store. To achieve render optimization, 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:
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
{
|
|
115
|
+
visibilityFilter: "ALL", // ALL, ACTIVE, COMPLETED
|
|
116
|
+
todos: [{ task: "Shop groceries", completed: false } /* ...more items */]
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
We have a `<Filter/>` component that connects to the store with a selector `(state) => state.visibilityFilter`.
|
|
121
|
+
|
|
122
|
+
When any action updates the `todos` slice, the selector in the `<Filter/>` component needs to re-run to determine if a re-rendering of the component is required. This is not optimal as `<Filter/>` component should not even be bothered when the todos are added/removed/updated.
|
|
123
|
+
|
|
124
|
+
In contrast, decentralized state management may approach the same problem with two separate states:
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
const visibilityFilter = state("ALL"); // ALL, ACTIVE, COMPLETED
|
|
128
|
+
const todos = state([
|
|
129
|
+
{ task: "Shop groceries", completed: false }
|
|
130
|
+
/* ...more items */
|
|
131
|
+
]);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
An update of `todos`, which is localized and isolated from other states, does not affect the components connected to `visibilityFilter` and vice versa.
|
|
135
|
+
|
|
136
|
+
The difference might sound insignificant, but imaging every single state update could cause every selector in every component in the entire app to run again, it suggests that decentralized state model scales better for large apps. In addition, some other benefits such as code-splitting are made easier by this state model.
|
|
137
|
+
|
|
138
|
+
## Why this over Zustand?
|
|
139
|
+
|
|
140
|
+
- State updates localized and isolated from other irrelevant states.
|
|
141
|
+
- No potential naming conflicts among states/actions within the big store.
|
|
142
|
+
- No need to use a React Hook to extract actions from the store.
|
|
143
|
+
- Actions come from outside React and no need to add them into the `useCallback/useEffect` dep array.
|
|
144
|
+
|
|
145
|
+
## ✨Highlights✨
|
|
146
|
+
|
|
147
|
+
- Decentralized state management
|
|
148
|
+
- Un-opinionated and easy-to-use API
|
|
149
|
+
- No need of wrapping app in Context or prop drilling
|
|
150
|
+
- React components re-render only on changes
|
|
151
|
+
- Compatible with React 18 concurrent rendering
|
|
152
|
+
- Selectors are memoized by default
|
|
153
|
+
- Feature extensible with middleware or plugins
|
|
154
|
+
- States persistable to browser storage
|
|
155
|
+
- Support Redux dev tools via middleware
|
|
156
|
+
- Less than 1KB: simple and small
|
|
157
|
+
|
|
158
|
+
# Recipes
|
|
159
|
+
|
|
160
|
+
## States should be updated immutably
|
|
161
|
+
|
|
162
|
+
```js
|
|
163
|
+
import { state } from "reactish-state";
|
|
164
|
+
|
|
165
|
+
const todosState = state([{ task: "Clean the house", completed: true }]);
|
|
166
|
+
todosState.set((todos) => [
|
|
167
|
+
...todos,
|
|
168
|
+
{ task: "Shop groceries", completed: false }
|
|
169
|
+
]);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
You can use the `immer` package to reduce boilerplate code:
|
|
173
|
+
|
|
174
|
+
```js
|
|
175
|
+
import produce from "immer";
|
|
176
|
+
|
|
177
|
+
todosState.set(
|
|
178
|
+
produce((todos) => {
|
|
179
|
+
todos.push({ task: "Shop groceries", completed: false });
|
|
180
|
+
})
|
|
181
|
+
);
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Or, simply use the [immer middleware](#immer-middleware).
|
|
185
|
+
|
|
186
|
+
## Selectors are memoized
|
|
187
|
+
|
|
188
|
+
Selector has an API that is similar to the [reselect](https://github.com/reduxjs/reselect#readme) package. You pass in one or more "input" states or selectors, and an "output" selector function that receives the extracted values and should return a derived value. The return value is memoized so that it won't cause React components to re-render even if non-primitive value is returned.
|
|
189
|
+
|
|
190
|
+
```js
|
|
191
|
+
import { selector } from "reactish-state";
|
|
192
|
+
|
|
193
|
+
// Return a number
|
|
194
|
+
const totalNumSelector = selector(todosState, (todos) => todos.length);
|
|
195
|
+
|
|
196
|
+
// Return a new array
|
|
197
|
+
const completedTodosSelector = selector(todosState, (todos) =>
|
|
198
|
+
todos.filter((todo) => todo.completed)
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// Return an object
|
|
202
|
+
const todoStats = selector(
|
|
203
|
+
totalNumSelector,
|
|
204
|
+
completedTodosSelector,
|
|
205
|
+
(totalNum, completedTodos) => ({
|
|
206
|
+
completedNum: completedTodos.length,
|
|
207
|
+
percentCompleted: (completedTodos.length / totalNum) * 100
|
|
208
|
+
})
|
|
209
|
+
);
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Async state updates
|
|
213
|
+
|
|
214
|
+
Just call `set` when you're ready:
|
|
215
|
+
|
|
216
|
+
```js
|
|
217
|
+
const todosState = state([]);
|
|
218
|
+
|
|
219
|
+
async function fetchTodos(url) {
|
|
220
|
+
const response = await fetch(url);
|
|
221
|
+
todosState.set(await response.json());
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
You can also create async actions bound to a state:
|
|
226
|
+
|
|
227
|
+
```js
|
|
228
|
+
const todosState = state([], (set) => ({
|
|
229
|
+
fetch: async (url) => {
|
|
230
|
+
const response = await fetch(url);
|
|
231
|
+
set(await response.json());
|
|
232
|
+
}
|
|
233
|
+
}));
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Accessing other state or selector inside actions
|
|
237
|
+
|
|
238
|
+
You might not need it, but nothing stops you from reading or writing to other state inside an action.
|
|
239
|
+
|
|
240
|
+
```js
|
|
241
|
+
const inputState = state("New item");
|
|
242
|
+
const todosState = state(
|
|
243
|
+
[{ task: "Shop groceries", completed: false }],
|
|
244
|
+
(set) => ({
|
|
245
|
+
add: () => {
|
|
246
|
+
set((todos) => [...todos, { task: inputState.get(), completed: false }]);
|
|
247
|
+
inputState.set(""); // Reset input after adding a todo
|
|
248
|
+
}
|
|
249
|
+
})
|
|
250
|
+
);
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Interacting with state or selector outside React
|
|
254
|
+
|
|
255
|
+
```js
|
|
256
|
+
const countState = state(0);
|
|
257
|
+
const tripleSelector = selector(countState, (count) => count * 3);
|
|
258
|
+
|
|
259
|
+
// Get a non-reactish fresh value
|
|
260
|
+
const count = countState.get();
|
|
261
|
+
const triple = tripleSelector.get();
|
|
262
|
+
|
|
263
|
+
// Listen to updates
|
|
264
|
+
const unsub1 = countState.subscribe(() => console.log(countState.get()));
|
|
265
|
+
const unsub2 = tripleSelector.subscribe(() =>
|
|
266
|
+
console.log(tripleSelector.get())
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// Update `countState`, will trigger both listeners
|
|
270
|
+
countState.set(10);
|
|
271
|
+
|
|
272
|
+
// Unsubscribe listeners
|
|
273
|
+
unsub1();
|
|
274
|
+
unsub2();
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
The only difference between state and selector is that selectors are read-only which don't have a `set` method.
|
|
278
|
+
|
|
279
|
+
## Destructuring actions for easier reference
|
|
280
|
+
|
|
281
|
+
The `set` or actions of a state don't rely on `this` to work, thus you are free to destructure them for easier reference.
|
|
282
|
+
|
|
283
|
+
_TIP_: destructure the actions outside React components so that you don't need to add them into the `useCallback/useEffect` dependency array.
|
|
284
|
+
|
|
285
|
+
```jsx
|
|
286
|
+
import { state, useSnapshot } from "reactish-state";
|
|
287
|
+
|
|
288
|
+
const countState = state(0, (set) => ({
|
|
289
|
+
increase: () => set((count) => count + 1),
|
|
290
|
+
reset: () => set(0)
|
|
291
|
+
}));
|
|
292
|
+
const { increase, reset } = countState.actions;
|
|
293
|
+
|
|
294
|
+
const Example = () => {
|
|
295
|
+
const count = useSnapshot(countState);
|
|
296
|
+
return (
|
|
297
|
+
<h1>
|
|
298
|
+
{count}
|
|
299
|
+
<button onClick={() => increase()}>Increase</button>
|
|
300
|
+
<button onClick={() => reset()}>Reset</button>
|
|
301
|
+
</h1>
|
|
302
|
+
);
|
|
303
|
+
};
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## Still perfer Redux-like reducers?
|
|
307
|
+
|
|
308
|
+
```js
|
|
309
|
+
const reducer = (state, { type, by = 1 }) => {
|
|
310
|
+
switch (type) {
|
|
311
|
+
case "INCREASE":
|
|
312
|
+
return state + by;
|
|
313
|
+
case "DECREASE":
|
|
314
|
+
return state - by;
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const countState = state(0, (set) => ({
|
|
319
|
+
dispatch: (action) => set((state) => reducer(state, action), action)
|
|
320
|
+
}));
|
|
321
|
+
|
|
322
|
+
const { dispatch } = countState.actions;
|
|
323
|
+
dispatch({ type: "INCREASE", by: 10 });
|
|
324
|
+
dispatch({ type: "DECREASE", by: 7 });
|
|
325
|
+
console.log(countState.get()); // Print 3
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## Middleware
|
|
329
|
+
|
|
330
|
+
You can enhance the functionalities of state with middleware. Instead of using the `state` export, you use the `createState` export from the library. Middleware is a function which receives `set`, `get` and `subscribe` and should return a new set function.
|
|
331
|
+
|
|
332
|
+
```js
|
|
333
|
+
import { createState } from "reactish-state";
|
|
334
|
+
|
|
335
|
+
const state = createState({
|
|
336
|
+
middleware:
|
|
337
|
+
({ set, get }) =>
|
|
338
|
+
(...args) => {
|
|
339
|
+
set(...args);
|
|
340
|
+
// Log state every time after calling `set`
|
|
341
|
+
console.log("New state", get());
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Now the `state` function has wired up a middleware
|
|
346
|
+
const countState = state(0, (set) => ({
|
|
347
|
+
increase: () => set((count) => count + 1)
|
|
348
|
+
}));
|
|
349
|
+
|
|
350
|
+
countState.set(99); // Print "New state 99"
|
|
351
|
+
countState.actions.increase(); // Print "New state 100"
|
|
352
|
+
|
|
353
|
+
// The same `state` function can be reused,
|
|
354
|
+
// thus you don't need to set up the middleware again
|
|
355
|
+
const filterState = state("ALL");
|
|
356
|
+
filterState.set("COMPLETED"); // Print "New state 'COMPLETED'"
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
## Persist middleware
|
|
360
|
+
|
|
361
|
+
You can save state in browser storage with the `persist` middleware.
|
|
362
|
+
|
|
363
|
+
```js
|
|
364
|
+
import { createState } from "reactish-state";
|
|
365
|
+
import { persist } from "reactish-state/middleware";
|
|
366
|
+
|
|
367
|
+
// Create the persist middleware,
|
|
368
|
+
// you can optionally provide a `prefix` prepended to the keys in storage
|
|
369
|
+
const persistMiddleware = persist({ prefix: "myApp-" });
|
|
370
|
+
const state = createState({ middleware: persistMiddleware });
|
|
371
|
+
|
|
372
|
+
const countState = state(
|
|
373
|
+
0,
|
|
374
|
+
(set) => ({
|
|
375
|
+
increase: () => set((count) => count + 1)
|
|
376
|
+
}),
|
|
377
|
+
{ key: "count" } // In the third parameter, give each state a unique key
|
|
378
|
+
);
|
|
379
|
+
const filterState = state("ALL", null, { key: "filter" });
|
|
380
|
+
|
|
381
|
+
// Hydrate all the states created with this middleware from storage
|
|
382
|
+
useEffect(() => {
|
|
383
|
+
// Call `hydrate` in an useEffect to avoid client-side mismatch
|
|
384
|
+
// if React components are also server-rendered
|
|
385
|
+
persistMiddleware.hydrate();
|
|
386
|
+
}, []);
|
|
387
|
+
// You can add the `useEffect` once into your root component
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
By default `localStorage` is used to persist states. You can change it to `sessionStorage` or other implementations using the `getStorage` option.
|
|
391
|
+
|
|
392
|
+
```js
|
|
393
|
+
const persistMiddleware = persist({ getStorage: () => sessionStorage });
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
## Immer middleware
|
|
397
|
+
|
|
398
|
+
You can mutably update state with the `immer` middleware.
|
|
399
|
+
|
|
400
|
+
```js
|
|
401
|
+
import { createState } from "reactish-state";
|
|
402
|
+
import { immer } from "reactish-state/middleware/immer";
|
|
403
|
+
|
|
404
|
+
const state = createState({ middleware: immer });
|
|
405
|
+
|
|
406
|
+
let todoId = 1;
|
|
407
|
+
const todos = state([], (set) => ({
|
|
408
|
+
add: (task) =>
|
|
409
|
+
set((todos) => {
|
|
410
|
+
todos.push({ id: todoId++, task, completed: false });
|
|
411
|
+
// Need to return the draft state for correct typing in TypeScript code
|
|
412
|
+
// return todos;
|
|
413
|
+
}),
|
|
414
|
+
|
|
415
|
+
toggle: (id) =>
|
|
416
|
+
set((todos) => {
|
|
417
|
+
const todo = todos.find((todo) => todo.id === id);
|
|
418
|
+
if (todo) todo.completed = !todo.completed;
|
|
419
|
+
})
|
|
420
|
+
}));
|
|
421
|
+
|
|
422
|
+
// Using the actions
|
|
423
|
+
todos.actions.add("Shop groceries");
|
|
424
|
+
todos.actions.toggle(1);
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
## Redux devtools middleware
|
|
428
|
+
|
|
429
|
+
Individual state will be combined into one big object in the Redux devtools for easy inspection.
|
|
430
|
+
|
|
431
|
+
```js
|
|
432
|
+
import { createState } from "reactish-state";
|
|
433
|
+
import { reduxDevtools } from "reactish-state/middleware";
|
|
434
|
+
|
|
435
|
+
const state = createState({ middleware: reduxDevtools({ name: "todoApp" }) });
|
|
436
|
+
|
|
437
|
+
const todos = state(
|
|
438
|
+
[],
|
|
439
|
+
(set) => ({
|
|
440
|
+
add: (task) =>
|
|
441
|
+
set(
|
|
442
|
+
(todos) => {
|
|
443
|
+
/* Add todo */
|
|
444
|
+
},
|
|
445
|
+
// Log action type in the second parameter of `set`
|
|
446
|
+
"todo/add"
|
|
447
|
+
),
|
|
448
|
+
toggle: (id) =>
|
|
449
|
+
set(
|
|
450
|
+
(todos) => {
|
|
451
|
+
/* Toggle todo */
|
|
452
|
+
},
|
|
453
|
+
// You can also log action type along with its payload
|
|
454
|
+
{ type: "todo/toggle", id }
|
|
455
|
+
)
|
|
456
|
+
}),
|
|
457
|
+
// Similar to the persist middleware, give each state a unique key
|
|
458
|
+
{ key: "todos" }
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
// `todos` and `filter` will be combined into one state in the Redux devtools
|
|
462
|
+
const filter = state("ALL", null, { key: "filter" });
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
## Using multiple middleware
|
|
466
|
+
|
|
467
|
+
Middleware is chain-able. You can use the `applyMiddleware` utility to chain multiple middleware and supply the result to `createState`.
|
|
468
|
+
|
|
469
|
+
```js
|
|
470
|
+
import { applyMiddleware } from "reactish-state/middleware";
|
|
471
|
+
|
|
472
|
+
const state = createState({
|
|
473
|
+
middleware: applyMiddleware([immer, reduxDevtools(), persist()])
|
|
474
|
+
});
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
## Using different middleware in different states
|
|
478
|
+
|
|
479
|
+
This is naturally achievable thanks to the decentralized state model.
|
|
480
|
+
|
|
481
|
+
```js
|
|
482
|
+
const persistState = createState({ middleware: persist() });
|
|
483
|
+
const immerState = createState({ middleware: immer });
|
|
484
|
+
|
|
485
|
+
const visibilityFilter = persistState("ALL"); // Will be persisted
|
|
486
|
+
const todos = immerState([]); // Can be mutated
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
It also helps eliminate the need for implementing whitelist/blacklist in a persist middleware.
|
|
490
|
+
|
|
491
|
+
## Plugins
|
|
492
|
+
|
|
493
|
+
While the middleware is used to enhance state, you can hook into selectors using the plugins. The main difference is that plugins don't return a `set` function because selectors are read-only. Similarly, you use the `createSelector` export from the library rather than `selector`.
|
|
494
|
+
|
|
495
|
+
```js
|
|
496
|
+
import { state, createSelector } from "reactish-state";
|
|
497
|
+
|
|
498
|
+
const selector = createSelector({
|
|
499
|
+
plugin: ({ get, subscribe }, config) => {
|
|
500
|
+
subscribe(() => {
|
|
501
|
+
// Log selector value every time after it has changed
|
|
502
|
+
// `config` can hold contextual data from a selector
|
|
503
|
+
console.log(`${config?.key} selector:`, get());
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
const countState = state(0);
|
|
509
|
+
const doubleSelector = selector(
|
|
510
|
+
countState,
|
|
511
|
+
(count) => count * 2,
|
|
512
|
+
// Provide contextual data in the last parameter to identity selector
|
|
513
|
+
{
|
|
514
|
+
key: "double"
|
|
515
|
+
}
|
|
516
|
+
);
|
|
517
|
+
const squareSelector = selector(countState, (count) => count * count, {
|
|
518
|
+
key: "square"
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
countState.set(5); // Will log - double selector: 10, square selector: 25
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
Likewise, there is an `applyPlugin` function for applying multiple plugins.
|
|
525
|
+
|
|
526
|
+
## Redux devtools plugin
|
|
527
|
+
|
|
528
|
+
Individual selector will be combined into one big object in the Redux devtools for easy inspection.
|
|
529
|
+
|
|
530
|
+
```js
|
|
531
|
+
import { createSelector } from "reactish-state";
|
|
532
|
+
import { reduxDevtools } from "reactish-state/plugin";
|
|
533
|
+
|
|
534
|
+
const selector = createSelector({ plugin: reduxDevtools() });
|
|
535
|
+
// Then use the `selector` as always...
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
# Examples
|
|
539
|
+
|
|
540
|
+
- Counter – [sandbox](https://codesandbox.io/s/reactish-counter-3let0o) | [source](https://github.com/szhsin/reactish-state/tree/master/examples/examples/counter)
|
|
541
|
+
- Todo app – [sandbox](https://codesandbox.io/s/reactish-todo-thyhbl) | [source](https://github.com/szhsin/reactish-state/tree/master/examples/examples/todo)
|
|
542
|
+
- Async – [sandbox](https://codesandbox.io/s/reactish-async-2cghkg) | [source](https://github.com/szhsin/reactish-state/tree/master/examples/examples/async)
|
package/package.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reactish-state",
|
|
3
|
-
"version": "0.10.
|
|
4
|
-
"description": "",
|
|
3
|
+
"version": "0.10.3",
|
|
4
|
+
"description": "Simple, decentralized state management for React.",
|
|
5
5
|
"author": "Zheng Song",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"repository": "szhsin/reactish-state",
|
|
8
|
+
"homepage": "https://github.com/szhsin/reactish-state#readme",
|
|
7
9
|
"main": "./dist/cjs/index.js",
|
|
8
10
|
"module": "./dist/esm/index.js",
|
|
9
11
|
"types": "./types/index.d.ts",
|
|
@@ -14,6 +16,12 @@
|
|
|
14
16
|
"middleware/",
|
|
15
17
|
"plugin/"
|
|
16
18
|
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"react",
|
|
21
|
+
"state",
|
|
22
|
+
"management",
|
|
23
|
+
"redux"
|
|
24
|
+
],
|
|
17
25
|
"scripts": {
|
|
18
26
|
"start": "run-p watch \"types -- --watch\"",
|
|
19
27
|
"bundle": "rollup -c",
|
|
@@ -65,35 +73,35 @@
|
|
|
65
73
|
"use-sync-external-store": "^1.2.0"
|
|
66
74
|
},
|
|
67
75
|
"devDependencies": {
|
|
68
|
-
"@babel/core": "^7.
|
|
69
|
-
"@babel/preset-env": "^7.
|
|
76
|
+
"@babel/core": "^7.20.12",
|
|
77
|
+
"@babel/preset-env": "^7.20.2",
|
|
70
78
|
"@babel/preset-react": "^7.18.6",
|
|
71
79
|
"@babel/preset-typescript": "^7.18.6",
|
|
72
|
-
"@redux-devtools/extension": "^3.2.
|
|
73
|
-
"@rollup/plugin-babel": "^6.0.
|
|
74
|
-
"@rollup/plugin-node-resolve": "^15.0.
|
|
80
|
+
"@redux-devtools/extension": "^3.2.5",
|
|
81
|
+
"@rollup/plugin-babel": "^6.0.3",
|
|
82
|
+
"@rollup/plugin-node-resolve": "^15.0.1",
|
|
75
83
|
"@testing-library/jest-dom": "^5.16.5",
|
|
76
84
|
"@testing-library/react": "^13.4.0",
|
|
77
|
-
"@types/jest": "^29.
|
|
78
|
-
"@types/react": "^18.0.
|
|
85
|
+
"@types/jest": "^29.2.5",
|
|
86
|
+
"@types/react": "^18.0.26",
|
|
79
87
|
"@types/use-sync-external-store": "^0.0.3",
|
|
80
|
-
"@typescript-eslint/eslint-plugin": "^5.
|
|
81
|
-
"@typescript-eslint/parser": "^5.
|
|
88
|
+
"@typescript-eslint/eslint-plugin": "^5.48.2",
|
|
89
|
+
"@typescript-eslint/parser": "^5.48.2",
|
|
82
90
|
"babel-plugin-pure-annotations": "^0.1.2",
|
|
83
|
-
"eslint": "^8.
|
|
84
|
-
"eslint-config-prettier": "^8.
|
|
85
|
-
"eslint-plugin-jest": "^27.1
|
|
86
|
-
"eslint-plugin-react": "^7.
|
|
91
|
+
"eslint": "^8.32.0",
|
|
92
|
+
"eslint-config-prettier": "^8.6.0",
|
|
93
|
+
"eslint-plugin-jest": "^27.2.1",
|
|
94
|
+
"eslint-plugin-react": "^7.32.1",
|
|
87
95
|
"eslint-plugin-react-hooks": "^4.6.0",
|
|
88
96
|
"eslint-plugin-react-hooks-addons": "^0.3.1",
|
|
89
|
-
"immer": "^9.0.
|
|
90
|
-
"jest": "^29.
|
|
91
|
-
"jest-environment-jsdom": "^29.
|
|
97
|
+
"immer": "^9.0.18",
|
|
98
|
+
"jest": "^29.3.1",
|
|
99
|
+
"jest-environment-jsdom": "^29.3.1",
|
|
92
100
|
"npm-run-all": "^4.1.5",
|
|
93
|
-
"prettier": "^2.
|
|
101
|
+
"prettier": "^2.8.3",
|
|
94
102
|
"react": "^18.2.0",
|
|
95
103
|
"react-dom": "^18.2.0",
|
|
96
|
-
"rollup": "^3.
|
|
97
|
-
"typescript": "^4.
|
|
104
|
+
"rollup": "^3.10.0",
|
|
105
|
+
"typescript": "^4.9.4"
|
|
98
106
|
}
|
|
99
107
|
}
|
package/types/common.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
1
|
+
export type Getter<T> = () => T;
|
|
2
|
+
export type Setter<T> = (newValue: T | ((value: T) => T), action?: string | {
|
|
3
3
|
type: string;
|
|
4
4
|
[key: string]: unknown;
|
|
5
5
|
}) => void;
|
|
6
|
-
export
|
|
7
|
-
export
|
|
6
|
+
export type Listener = () => void;
|
|
7
|
+
export type Subscriber = (listener: Listener) => () => void;
|
|
8
8
|
export interface Reactish<T> {
|
|
9
9
|
get: Getter<T>;
|
|
10
10
|
subscribe: Subscriber;
|
|
@@ -2,7 +2,7 @@ import type { Middleware } from '../common';
|
|
|
2
2
|
interface PersistMiddleware extends Middleware {
|
|
3
3
|
hydrate(): void;
|
|
4
4
|
}
|
|
5
|
-
|
|
5
|
+
type Persist = (options?: {
|
|
6
6
|
prefix?: string;
|
|
7
7
|
getStorage?: () => Pick<Storage, 'getItem' | 'setItem'>;
|
|
8
8
|
}) => PersistMiddleware;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Reactish, Plugin, Config } from '../common';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
type ReactishArray = Reactish<unknown>[];
|
|
3
|
+
type ReactishValueArray<R extends ReactishArray> = {
|
|
4
4
|
[index in keyof R]: ReturnType<R[index]['get']>;
|
|
5
5
|
};
|
|
6
|
-
|
|
6
|
+
type SelectorFunc<R extends ReactishArray, T> = (...args: ReactishValueArray<R>) => T;
|
|
7
7
|
interface Selector {
|
|
8
8
|
<R extends ReactishArray, T>(...items: [...R, SelectorFunc<R, T>]): Reactish<T>;
|
|
9
9
|
<R extends ReactishArray, T>(...items: [...R, SelectorFunc<R, T>, Config]): Reactish<T>;
|
package/types/vanilla/state.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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
|
interface State<T, A = unknown, C extends ActionCreator<T, A> = undefined> extends Reactish<T> {
|
|
4
4
|
set: Setter<T>;
|
|
5
5
|
actions: C extends undefined ? never : A;
|