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
+ [![NPM](https://img.shields.io/npm/v/reactish-state.svg)](https://www.npmjs.com/package/reactish-state) [![NPM](https://img.shields.io/bundlephobia/minzip/reactish-state)](https://bundlephobia.com/package/reactish-state)
4
6
 
5
7
  ## ✨Highlights✨
6
8
 
7
9
  - Decentralized state management
8
- - Un-opinionated and easy-to-use API
9
- - No need of wrapping app in Context or prop drilling
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
- - States persistable to browser storage
15
- - Support Redux dev tools via middleware
16
- - [~1KB](https://bundlephobia.com/package/reactish-state): simple and small
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
- `npm install reactish-state` or `yarn add reactish-state`
22
+ ```bash
23
+ npm install reactish-state
24
+ ```
21
25
 
22
26
  ## Quick start
23
27
 
24
- ### We begin by creating some states
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
- // or using the functional update of `set`
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
- // Using the actions
55
- countState.actions.increase();
58
+ // Use the custom actions
59
+ countState.increase();
56
60
  ```
57
61
 
58
- ### `selector` can create derived states
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 any of the states it depends on have changed.
78
+ A selector will re-compute only when one of the states it depends on has changed.
75
79
 
76
- ### Use the state and selector in your React components
80
+ ### Use the state and selectors in your React components
77
81
 
78
- 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.
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.actions.increase()}>Increase</button>
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 have changed. No provider or context are needed!
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
- The state management solutions in the React ecosystem have popularized two state models:
111
+ State management solutions in the React ecosystem have popularized two state models:
107
112
 
108
- - **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.
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**: consisting of many small(atomic) 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.
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, 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 around 1KB.
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 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:
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-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.
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 of `todos`, which is localized and isolated from other states, does not affect the components connected to `visibilityFilter` and vice versa.
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
- 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.
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 the big store.
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 come from outside React and no need to add them into the `useCallback/useEffect` dep array.
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
- ## States should be updated immutably
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 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.
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 you're ready:
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
- fetch: async (url) => {
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 selector inside actions
246
+ ## Accessing other state or selectors inside actions
237
247
 
238
- You might not need it, but nothing stops you from reading or writing to other state inside an action.
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 selector outside React
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 to updates
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
- // Update `countState`, will trigger both listeners
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
- 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
287
+ ## Destructuring actions for easier access
280
288
 
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.
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_: destructure the actions outside React components so that you don't need to add them into the `useCallback/useEffect` dependency array.
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.actions;
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 states
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 which depend on props or local states can be created by the `useSelector` hook.
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), in which you can specify what props or local states the selector depends on. In the above example, `FilteredTodoList` component will re-render only if the global `todosState` state or local `filter` prop have been updated.
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`. Add the following configuration into your ESLint config file:
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
- "warn",
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.actions;
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 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.
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 a middleware
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.actions.increase(); // Print "New state 100"
408
+ countState.increase(); // Print "New state 100"
401
409
 
402
410
  // The same `state` function can be reused,
403
- // thus you don't need to set up the middleware again
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 in browser storage with the `persist` middleware.
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
- // you can optionally provide a `prefix` prepended to the keys in storage
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, give each state a unique key
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 an useEffect to avoid client-side mismatch
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 into your root component
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 change it to `sessionStorage` or other implementations using the `getStorage` option.
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 mutably update state with the `immer` middleware.
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
- // Need to return the draft state for correct typing in TypeScript code
461
- // return todos;
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
- // Using the actions
472
- todos.actions.add("Shop groceries");
473
- todos.actions.toggle(1);
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 state will be combined into one big object in the Redux devtools for easy inspection.
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, give each state a unique key
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 one state in the Redux devtools
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 chain-able. You can use the `applyMiddleware` utility to chain multiple middleware and supply the result to `createState`.
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
- It also helps eliminate the need for implementing whitelist/blacklist in a persist middleware.
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 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`.
550
+ While middleware enhances state, plugins allow you to hook into selectors. The key difference is that plugins dont 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 after it has changed
551
- // `config` can hold contextual data from a selector
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 identity selector
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); // Will log - double selector: 10, square selector: 25
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 selector will be combined into one big object in the Redux devtools for easy inspection.
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 always...
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
  };
@@ -23,10 +23,10 @@ const createState = ({
23
23
  subscribe
24
24
  }, config);
25
25
  return {
26
+ ...actionCreator?.(set, get),
26
27
  get,
27
28
  set,
28
- subscribe,
29
- actions: actionCreator?.(set, get)
29
+ subscribe
30
30
  };
31
31
  };
32
32
  const state = /*#__PURE__*/createState();
@@ -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
  };
@@ -21,10 +21,10 @@ const createState = ({
21
21
  subscribe
22
22
  }, config);
23
23
  return {
24
+ ...actionCreator?.(set, get),
24
25
  get,
25
26
  set,
26
- subscribe,
27
- actions: actionCreator?.(set, get)
27
+ subscribe
28
28
  };
29
29
  };
30
30
  const state = /*#__PURE__*/createState();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reactish-state",
3
- "version": "1.0.0-alpha.0",
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",
@@ -0,0 +1 @@
1
+ export * from '../types/shim';
@@ -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
- interface State<T, A = unknown, C extends ActionCreator<T, A> = undefined> extends Reactish<T> {
3
+ type VanillaState<T> = Reactish<T> & {
4
4
  set: Setter<T>;
5
- actions: C extends undefined ? never : A;
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, ActionCreator<T, A>>;
10
- declare const state: <T, A>(initialValue: T, actionCreator?: ActionCreator<T, A>, config?: Config) => State<T, A, ActionCreator<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 };