reactish-state 0.10.4 → 0.11.0

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,6 +1,6 @@
1
1
  # Reactish-State
2
2
 
3
- > Simple, decentralized state management for React.
3
+ > Simple, decentralized(atomic) state management for React.
4
4
 
5
5
  ## Install
6
6
 
@@ -94,9 +94,9 @@ The state management solutions in the React ecosystem have popularized two state
94
94
 
95
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
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.
97
+ - **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.
98
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.
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 around 1KB.
100
100
 
101
101
  | | State model | Bundle size |
102
102
  | --- | --- | --- |
@@ -153,7 +153,7 @@ The difference might sound insignificant, but imaging every single state update
153
153
  - Feature extensible with middleware or plugins
154
154
  - States persistable to browser storage
155
155
  - Support Redux dev tools via middleware
156
- - Less than 1KB: simple and small
156
+ - [~1KB](https://bundlephobia.com/package/reactish-state): simple and small
157
157
 
158
158
  # Recipes
159
159
 
@@ -303,6 +303,55 @@ const Example = () => {
303
303
  };
304
304
  ```
305
305
 
306
+ ## Selector that depends on props or local states
307
+
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.
309
+
310
+ ```jsx
311
+ import { state, useSelector } from "reactish-state";
312
+
313
+ const todosState = state([{ task: "Shop groceries", completed: false }]);
314
+
315
+ const FilteredTodoList = ({ filter = "ALL" }) => {
316
+ const filteredTodos = useSelector(
317
+ () => [
318
+ todosState,
319
+ (todos) => {
320
+ switch (filter) {
321
+ case "ALL":
322
+ return todos;
323
+ case "COMPLETED":
324
+ return todos.filter((todo) => todo.completed);
325
+ case "ACTIVE":
326
+ return todos.filter((todo) => !todo.completed);
327
+ }
328
+ }
329
+ ],
330
+ [filter]
331
+ );
332
+ // Render filtered todos...
333
+ };
334
+ ```
335
+
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.
337
+
338
+ ### Linting the dependency array of useSelector
339
+
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:
341
+
342
+ ```json
343
+ {
344
+ "rules": {
345
+ "react-hooks/exhaustive-deps": [
346
+ "warn",
347
+ {
348
+ "additionalHooks": "useSelector"
349
+ }
350
+ ]
351
+ }
352
+ }
353
+ ```
354
+
306
355
  ## Still perfer Redux-like reducers?
307
356
 
308
357
  ```js
package/dist/cjs/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var shim = require('use-sync-external-store/shim');
4
+ var react = require('react');
4
5
 
5
6
  const createState = ({
6
7
  middleware
@@ -39,6 +40,12 @@ const isEqual = (args1, args2) => {
39
40
  }
40
41
  return true;
41
42
  };
43
+ const createSubscriber = items => listener => {
44
+ const unsubscribers = items.map(item => item.subscribe(listener));
45
+ return () => unsubscribers.forEach(unsubscribe => unsubscribe());
46
+ };
47
+ const getReactishValues = items => items.map(item => item.get());
48
+
42
49
  const createSelector = ({
43
50
  plugin
44
51
  } = {}) => (...items) => {
@@ -52,19 +59,16 @@ const createSelector = ({
52
59
  let cache;
53
60
  const selector = {
54
61
  get: () => {
55
- const args = items.map(item => item.get());
56
- if (cache && isEqual(args, cache.args)) return cache.ret;
57
- const ret = selectorFunc(...args);
62
+ const args = getReactishValues(items);
63
+ if (cache && isEqual(args, cache.args)) return cache.val;
64
+ const val = selectorFunc(...args);
58
65
  cache = {
59
66
  args,
60
- ret
67
+ val
61
68
  };
62
- return ret;
69
+ return val;
63
70
  },
64
- subscribe: listener => {
65
- const unsubscribers = items.map(item => item.subscribe(listener));
66
- return () => unsubscribers.forEach(unsubscribe => unsubscribe());
67
- }
71
+ subscribe: createSubscriber(items)
68
72
  };
69
73
  plugin == null ? void 0 : plugin(selector, config);
70
74
  return selector;
@@ -76,8 +80,37 @@ const useSnapshot = ({
76
80
  get
77
81
  }) => shim.useSyncExternalStore(subscribe, get, get);
78
82
 
83
+ const useSelector = (selectorParamFactory, deps) => {
84
+ const items = selectorParamFactory();
85
+ const cutoff = items.length - 1;
86
+ const selectorFunc = items[cutoff];
87
+ items.length = cutoff;
88
+ const [context] = react.useState(() => ({
89
+ sub: createSubscriber(items)
90
+ }));
91
+ const get = () => {
92
+ const {
93
+ cache
94
+ } = context;
95
+ const reactishValues = getReactishValues(items);
96
+ const args = reactishValues.concat(deps || selectorFunc);
97
+ if (cache && isEqual(args, cache.args)) return cache.val;
98
+ const val = selectorFunc(...reactishValues);
99
+ context.cache = {
100
+ args,
101
+ val
102
+ };
103
+ return val;
104
+ };
105
+ return useSnapshot({
106
+ get,
107
+ subscribe: context.sub
108
+ });
109
+ };
110
+
79
111
  exports.createSelector = createSelector;
80
112
  exports.createState = createState;
81
113
  exports.selector = selector;
82
114
  exports.state = state;
115
+ exports.useSelector = useSelector;
83
116
  exports.useSnapshot = useSnapshot;
package/dist/esm/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { createState, state } from './vanilla/state.js';
2
2
  export { createSelector, selector } from './vanilla/selector.js';
3
3
  export { useSnapshot } from './react/useSnapshot.js';
4
+ export { useSelector } from './react/useSelector.js';
@@ -0,0 +1,33 @@
1
+ import { useState } from 'react';
2
+ import { createSubscriber, getReactishValues, isEqual } from '../utils.js';
3
+ import { useSnapshot } from './useSnapshot.js';
4
+
5
+ const useSelector = (selectorParamFactory, deps) => {
6
+ const items = selectorParamFactory();
7
+ const cutoff = items.length - 1;
8
+ const selectorFunc = items[cutoff];
9
+ items.length = cutoff;
10
+ const [context] = useState(() => ({
11
+ sub: createSubscriber(items)
12
+ }));
13
+ const get = () => {
14
+ const {
15
+ cache
16
+ } = context;
17
+ const reactishValues = getReactishValues(items);
18
+ const args = reactishValues.concat(deps || selectorFunc);
19
+ if (cache && isEqual(args, cache.args)) return cache.val;
20
+ const val = selectorFunc(...reactishValues);
21
+ context.cache = {
22
+ args,
23
+ val
24
+ };
25
+ return val;
26
+ };
27
+ return useSnapshot({
28
+ get,
29
+ subscribe: context.sub
30
+ });
31
+ };
32
+
33
+ export { useSelector };
@@ -0,0 +1,13 @@
1
+ const isEqual = (args1, args2) => {
2
+ for (let i = 0; i < args1.length; i++) {
3
+ if (!Object.is(args1[i], args2[i])) return false;
4
+ }
5
+ return true;
6
+ };
7
+ const createSubscriber = items => listener => {
8
+ const unsubscribers = items.map(item => item.subscribe(listener));
9
+ return () => unsubscribers.forEach(unsubscribe => unsubscribe());
10
+ };
11
+ const getReactishValues = items => items.map(item => item.get());
12
+
13
+ export { createSubscriber, getReactishValues, isEqual };
@@ -1,9 +1,5 @@
1
- const isEqual = (args1, args2) => {
2
- for (let i = 0; i < args1.length; i++) {
3
- if (!Object.is(args1[i], args2[i])) return false;
4
- }
5
- return true;
6
- };
1
+ import { getReactishValues, createSubscriber, isEqual } from '../utils.js';
2
+
7
3
  const createSelector = ({
8
4
  plugin
9
5
  } = {}) => (...items) => {
@@ -17,19 +13,16 @@ const createSelector = ({
17
13
  let cache;
18
14
  const selector = {
19
15
  get: () => {
20
- const args = items.map(item => item.get());
21
- if (cache && isEqual(args, cache.args)) return cache.ret;
22
- const ret = selectorFunc(...args);
16
+ const args = getReactishValues(items);
17
+ if (cache && isEqual(args, cache.args)) return cache.val;
18
+ const val = selectorFunc(...args);
23
19
  cache = {
24
20
  args,
25
- ret
21
+ val
26
22
  };
27
- return ret;
23
+ return val;
28
24
  },
29
- subscribe: listener => {
30
- const unsubscribers = items.map(item => item.subscribe(listener));
31
- return () => unsubscribers.forEach(unsubscribe => unsubscribe());
32
- }
25
+ subscribe: createSubscriber(items)
33
26
  };
34
27
  plugin == null ? void 0 : plugin(selector, config);
35
28
  return selector;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reactish-state",
3
- "version": "0.10.4",
3
+ "version": "0.11.0",
4
4
  "description": "Simple, decentralized state management for React.",
5
5
  "author": "Zheng Song",
6
6
  "license": "MIT",
@@ -73,35 +73,35 @@
73
73
  "use-sync-external-store": "^1.2.0"
74
74
  },
75
75
  "devDependencies": {
76
- "@babel/core": "^7.20.12",
77
- "@babel/preset-env": "^7.20.2",
76
+ "@babel/core": "^7.21.4",
77
+ "@babel/preset-env": "^7.21.4",
78
78
  "@babel/preset-react": "^7.18.6",
79
- "@babel/preset-typescript": "^7.18.6",
79
+ "@babel/preset-typescript": "^7.21.4",
80
80
  "@redux-devtools/extension": "^3.2.5",
81
81
  "@rollup/plugin-babel": "^6.0.3",
82
- "@rollup/plugin-node-resolve": "^15.0.1",
82
+ "@rollup/plugin-node-resolve": "^15.0.2",
83
83
  "@testing-library/jest-dom": "^5.16.5",
84
- "@testing-library/react": "^13.4.0",
85
- "@types/jest": "^29.2.5",
86
- "@types/react": "^18.0.26",
84
+ "@testing-library/react": "^14.0.0",
85
+ "@types/jest": "^29.5.0",
86
+ "@types/react": "^18.0.34",
87
87
  "@types/use-sync-external-store": "^0.0.3",
88
- "@typescript-eslint/eslint-plugin": "^5.48.2",
89
- "@typescript-eslint/parser": "^5.48.2",
88
+ "@typescript-eslint/eslint-plugin": "^5.58.0",
89
+ "@typescript-eslint/parser": "^5.58.0",
90
90
  "babel-plugin-pure-annotations": "^0.1.2",
91
- "eslint": "^8.32.0",
92
- "eslint-config-prettier": "^8.6.0",
91
+ "eslint": "^8.38.0",
92
+ "eslint-config-prettier": "^8.8.0",
93
93
  "eslint-plugin-jest": "^27.2.1",
94
- "eslint-plugin-react": "^7.32.1",
94
+ "eslint-plugin-react": "^7.32.2",
95
95
  "eslint-plugin-react-hooks": "^4.6.0",
96
96
  "eslint-plugin-react-hooks-addons": "^0.3.1",
97
- "immer": "^9.0.18",
98
- "jest": "^29.3.1",
99
- "jest-environment-jsdom": "^29.3.1",
97
+ "immer": "^9.0.21",
98
+ "jest": "^29.5.0",
99
+ "jest-environment-jsdom": "^29.5.0",
100
100
  "npm-run-all": "^4.1.5",
101
- "prettier": "^2.8.3",
101
+ "prettier": "^2.8.7",
102
102
  "react": "^18.2.0",
103
103
  "react-dom": "^18.2.0",
104
- "rollup": "^3.10.0",
105
- "typescript": "^4.9.4"
104
+ "rollup": "^3.20.2",
105
+ "typescript": "^5.0.4"
106
106
  }
107
107
  }
package/types/common.d.ts CHANGED
@@ -20,3 +20,13 @@ export interface Middleware {
20
20
  export interface Plugin {
21
21
  <T>(reactish: Reactish<T>, config?: Config): void;
22
22
  }
23
+ export type ReactishArray = Reactish<unknown>[];
24
+ export type ReactishValueArray<RA extends ReactishArray> = {
25
+ [index in keyof RA]: ReturnType<RA[index]['get']>;
26
+ };
27
+ export type SelectorFunc<RA extends ReactishArray, T> = (...args: ReactishValueArray<RA>) => T;
28
+ export type SelectorParams<RA extends ReactishArray, T> = [...RA, SelectorFunc<RA, T>];
29
+ export interface Selector {
30
+ <RA extends ReactishArray, T>(...items: SelectorParams<RA, T>): Reactish<T>;
31
+ <RA extends ReactishArray, T>(...items: [...SelectorParams<RA, T>, Config]): Reactish<T>;
32
+ }
package/types/index.d.ts CHANGED
@@ -2,3 +2,4 @@ export * from './common';
2
2
  export * from './vanilla/state';
3
3
  export * from './vanilla/selector';
4
4
  export * from './react/useSnapshot';
5
+ export * from './react/useSelector';
@@ -0,0 +1,3 @@
1
+ import type { ReactishArray, SelectorFunc, SelectorParams } from '../common';
2
+ declare const useSelector: <RA extends ReactishArray, T>(selectorParamFactory: () => [...RA, SelectorFunc<RA, T>], deps?: unknown[]) => T;
3
+ export { useSelector };
@@ -0,0 +1,4 @@
1
+ import type { Subscriber, ReactishArray, ReactishValueArray } from './common';
2
+ export declare const isEqual: (args1: unknown[], args2: unknown[]) => boolean;
3
+ export declare const createSubscriber: (items: ReactishArray) => Subscriber;
4
+ export declare const getReactishValues: <RA extends ReactishArray>(items: ReactishArray) => ReactishValueArray<RA>;
@@ -1,13 +1,4 @@
1
- import type { Reactish, Plugin, Config } from '../common';
2
- type ReactishArray = Reactish<unknown>[];
3
- type ReactishValueArray<R extends ReactishArray> = {
4
- [index in keyof R]: ReturnType<R[index]['get']>;
5
- };
6
- type SelectorFunc<R extends ReactishArray, T> = (...args: ReactishValueArray<R>) => T;
7
- interface Selector {
8
- <R extends ReactishArray, T>(...items: [...R, SelectorFunc<R, T>]): Reactish<T>;
9
- <R extends ReactishArray, T>(...items: [...R, SelectorFunc<R, T>, Config]): Reactish<T>;
10
- }
1
+ import type { Plugin, Selector } from '../common';
11
2
  declare const createSelector: ({ plugin }?: {
12
3
  plugin?: Plugin | undefined;
13
4
  }) => Selector;