├── .eslintrc.json
├── .github
└── workflows
│ ├── cd.yml
│ └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── __tests__
├── 01_basic_spec.tsx
└── __snapshots__
│ └── 01_basic_spec.tsx.snap
├── examples
├── 01_minimal
│ ├── package.json
│ ├── public
│ │ └── index.html
│ └── src
│ │ └── index.js
├── 02_typescript
│ ├── package.json
│ ├── public
│ │ └── index.html
│ └── src
│ │ ├── App.tsx
│ │ ├── Counter.tsx
│ │ ├── Person.tsx
│ │ ├── context.ts
│ │ ├── index.ts
│ │ └── state.ts
├── 03_deep
│ ├── package.json
│ ├── public
│ │ └── index.html
│ └── src
│ │ ├── App.tsx
│ │ ├── Counter.tsx
│ │ ├── Person.tsx
│ │ ├── context.ts
│ │ ├── index.ts
│ │ └── state.ts
├── 04_immer
│ ├── package.json
│ ├── public
│ │ └── index.html
│ └── src
│ │ ├── App.tsx
│ │ ├── Counter.tsx
│ │ ├── Person.tsx
│ │ ├── context.ts
│ │ ├── index.ts
│ │ └── state.ts
├── 05_localstate
│ ├── package.json
│ ├── public
│ │ └── index.html
│ └── src
│ │ ├── App.tsx
│ │ ├── Counter.tsx
│ │ ├── context.ts
│ │ ├── index.ts
│ │ └── state.ts
├── 06_memoization
│ ├── package.json
│ ├── public
│ │ └── index.html
│ └── src
│ │ ├── App.tsx
│ │ ├── Counter.tsx
│ │ ├── Person.tsx
│ │ ├── context.ts
│ │ ├── index.ts
│ │ └── state.ts
├── 07_multistore
│ ├── package.json
│ ├── public
│ │ └── index.html
│ └── src
│ │ ├── App.tsx
│ │ ├── Counter.tsx
│ │ ├── Person.tsx
│ │ ├── context.ts
│ │ ├── index.ts
│ │ └── state.ts
├── 08_dynamic
│ ├── package.json
│ ├── public
│ │ └── index.html
│ └── src
│ │ ├── App.tsx
│ │ ├── Counter.tsx
│ │ ├── Person.tsx
│ │ ├── context.ts
│ │ ├── index.ts
│ │ └── state.ts
├── 09_thunk
│ ├── package.json
│ ├── public
│ │ └── index.html
│ └── src
│ │ ├── App.tsx
│ │ ├── Counter.tsx
│ │ ├── Person.tsx
│ │ ├── context.ts
│ │ ├── index.ts
│ │ └── state.ts
├── 11_todolist
│ ├── package.json
│ ├── public
│ │ └── index.html
│ └── src
│ │ ├── actions
│ │ └── index.ts
│ │ ├── components
│ │ ├── AddTodo.tsx
│ │ ├── App.tsx
│ │ ├── FilterLink.tsx
│ │ ├── Footer.tsx
│ │ ├── Todo.tsx
│ │ └── VisibleTodoList.tsx
│ │ ├── context.ts
│ │ ├── index.tsx
│ │ ├── reducers
│ │ ├── index.ts
│ │ ├── todos.ts
│ │ └── visibilityFilter.ts
│ │ └── types
│ │ └── index.ts
├── 12_async
│ ├── package.json
│ ├── public
│ │ └── index.html
│ └── src
│ │ ├── components
│ │ ├── App.tsx
│ │ ├── Picker.tsx
│ │ └── Posts.tsx
│ │ ├── context.ts
│ │ ├── hooks
│ │ ├── useFetchPostsIfNeeded.ts
│ │ ├── useInvalidateSubreddit.ts
│ │ └── useSelectSubreddit.ts
│ │ ├── index.tsx
│ │ └── store
│ │ ├── actions.ts
│ │ └── reducers.ts
└── 13_memo
│ ├── package.json
│ ├── public
│ └── index.html
│ └── src
│ ├── App.tsx
│ ├── TodoItem.tsx
│ ├── TodoList.tsx
│ ├── context.ts
│ ├── index.ts
│ └── state.ts
├── package.json
├── src
├── index.ts
├── memo.ts
├── patchStore.ts
├── useSelector.ts
├── useTrackedState.ts
└── utils.ts
├── tsconfig.json
├── webpack.config.js
└── yarn.lock
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "plugins": [
4 | "@typescript-eslint",
5 | "react-hooks"
6 | ],
7 | "extends": [
8 | "plugin:@typescript-eslint/recommended",
9 | "airbnb"
10 | ],
11 | "env": {
12 | "browser": true
13 | },
14 | "settings": {
15 | "import/resolver": {
16 | "node": {
17 | "extensions": [".js", ".ts", ".tsx"]
18 | }
19 | }
20 | },
21 | "rules": {
22 | "react-hooks/rules-of-hooks": "error",
23 | "react-hooks/exhaustive-deps": ["error", { "additionalHooks": "useIsomorphicLayoutEffect" }],
24 | "@typescript-eslint/explicit-function-return-type": "off",
25 | "@typescript-eslint/explicit-module-boundary-types": "off",
26 | "@typescript-eslint/no-explicit-any": "off",
27 | "@typescript-eslint/no-empty-interface": "off",
28 | "react/jsx-filename-extension": ["error", { "extensions": [".js", ".tsx"] }],
29 | "react/prop-types": "off",
30 | "react/jsx-one-expression-per-line": "off",
31 | "import/extensions": ["error", "never"],
32 | "import/prefer-default-export": "off",
33 | "import/no-unresolved": ["error", { "ignore": ["reactive-react-redux"] }],
34 | "no-param-reassign": "off",
35 | "no-plusplus": "off",
36 | "no-bitwise": "off",
37 | "default-case": "off",
38 | "no-undef": "off",
39 | "no-use-before-define": "off",
40 | "no-unused-vars": "off"
41 | },
42 | "overrides": [{
43 | "files": ["__tests__/**/*"],
44 | "env": {
45 | "jest": true
46 | },
47 | "rules": {
48 | "import/no-extraneous-dependencies": ["error", { "devDependencies": true }]
49 | }
50 | }]
51 | }
52 |
--------------------------------------------------------------------------------
/.github/workflows/cd.yml:
--------------------------------------------------------------------------------
1 | name: CD
2 |
3 | on:
4 | push:
5 | tags:
6 | - v*
7 |
8 | jobs:
9 | publish:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 |
14 | - name: Setup Node
15 | uses: actions/setup-node@v1
16 | with:
17 | node-version: '12.x'
18 | registry-url: 'https://registry.npmjs.org'
19 |
20 | - name: Get yarn cache
21 | id: yarn-cache
22 | run: echo "::set-output name=dir::$(yarn cache dir)"
23 |
24 | - name: Cache dependencies
25 | uses: actions/cache@v1
26 | with:
27 | path: ${{ steps.yarn-cache.outputs.dir }}
28 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
29 | restore-keys: |
30 | ${{ runner.os }}-yarn-
31 |
32 | - name: Install dependencies
33 | run: yarn install
34 |
35 | - name: Test
36 | run: yarn test
37 |
38 | - name: Compile
39 | run: yarn run compile
40 |
41 | - name: Publish
42 | run: npm publish
43 | env:
44 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
45 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | pull_request:
6 |
7 | jobs:
8 | test:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 |
13 | - name: Setup Node
14 | uses: actions/setup-node@v1
15 | with:
16 | node-version: '12.x'
17 |
18 | - name: Get yarn cache
19 | id: yarn-cache
20 | run: echo "::set-output name=dir::$(yarn cache dir)"
21 |
22 | - name: Cache dependencies
23 | uses: actions/cache@v1
24 | with:
25 | path: ${{ steps.yarn-cache.outputs.dir }}
26 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
27 | restore-keys: |
28 | ${{ runner.os }}-yarn-
29 |
30 | - name: Install dependencies
31 | run: yarn install
32 |
33 | - name: Test
34 | run: yarn test
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | node_modules
4 | /dist
5 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## [Unreleased]
4 | ### Changed
5 | - New API with useMutableSource (#48)
6 |
7 | ## [4.9.0] - 2020-05-10
8 | ### Changed
9 | - Introduce a special `memo` to be used instead of `trackMemo`
10 | - This is technically a breaking change, but released as a minor update.
11 |
12 | ## [4.8.0] - 2020-03-07
13 | ### Changed
14 | - Notify child components updates in callback and effect not in render
15 |
16 | ## [4.7.0] - 2020-03-01
17 | ### Changed
18 | - Add debug value to show tracked paths in useTrackedState
19 | - Unwrap Proxy before wrapping to mitigate possible pitfalls
20 |
21 | ## [4.6.1] - 2020-02-26
22 | ### Changed
23 | - Use improved useIsomorphicLayoutEffect in Provider (#47)
24 |
25 | ## [4.6.0] - 2020-02-24
26 | ### Changed
27 | - A workaround for React render warning (hopefully temporarily)
28 |
29 | ## [4.5.0] - 2020-02-03
30 | ### Changed
31 | - Improve TypeScript typing (#46)
32 |
33 | ## [4.4.1] - 2020-01-24
34 | ### Changed
35 | - Fix typing for createCustomContext (#44)
36 |
37 | ## [4.4.0] - 2019-10-17
38 | ### Added
39 | - A new API getUntrackedObject to revert proxy
40 |
41 | ## [4.3.0] - 2019-10-09
42 | ### Added
43 | - A new API trackMemo to mark objects as used in React.memo
44 |
45 | ## [4.2.1] - 2019-10-05
46 | ### Changed
47 | - Inline useForceUpdate to remove unnecessary deps
48 |
49 | ## [4.2.0] - 2019-07-27
50 | ### Removed
51 | - Remove the experimental useTrackedSelectors hook
52 |
53 | ## [4.1.0] - 2019-07-20
54 | ### Changed
55 | - No useLayoutEffect for invoking listeners (which leads de-opt sync mode)
56 | - Ref: https://github.com/dai-shi/reactive-react-redux/pull/31
57 |
58 | ## [4.0.0] - 2019-06-22
59 | ### Changed
60 | - Direct state context
61 | - Support custom context
62 | - Rename hooks to be somewhat compatible with react-redux hooks
63 |
64 | ## [3.0.0] - 2019-05-18
65 | ### Changed
66 | - New deep proxy instead of proxyequal
67 | - There is no breaking change in API, but it may behave differently from the previous version.
68 |
69 | ## [2.0.1] - 2019-04-15
70 | ### Changed
71 | - Rename src file names to be more consistent
72 |
73 | ## [2.0.0] - 2019-04-13
74 | ### Removed
75 | - Remove experimental useReduxStateMapped
76 | ### Changed
77 | - useLayoutEffect and keep latest state after update (see #20)
78 | - useForceUpdate uses counter instead of boolean (see #20)
79 | - Organize code in multiple files with some improvements
80 | - Update dependencies
81 | ### Added
82 | - New useReduxSelectors (experimental)
83 |
84 | ## [1.8.0] - 2019-04-02
85 | ### Changed
86 | - Improve useReduxStateMapped with proxyequal
87 |
88 | ## [1.7.0] - 2019-04-01
89 | ### Added
90 | - Implement useReduxStateMapped (experimental/unstable/undocumented)
91 | ### Changed
92 | - Rename project
93 | - Old "react-hooks-easy-redux"
94 | - New "reactive-react-redux"
95 |
96 | ## [1.6.0] - 2019-03-25
97 | ### Changed
98 | - Memoize patched store with batchedUpdates
99 |
100 | ## [1.5.0] - 2019-03-25
101 | ### Changed
102 | - No running callback in every commit phase (reverting #5)
103 |
104 | ## [1.4.0] - 2019-03-25
105 | ### Changed
106 | - Avoid recalculating collectValuables for optimization
107 | - Use unstable_batchedUpdates for optimization
108 | - This is not a breaking change as it has a fallback
109 | - Not tested with react-native (help wanted)
110 |
111 | ## [1.3.0] - 2019-03-03
112 | ### Changed
113 | - Better handling stale props issue
114 |
115 | ## [1.2.0] - 2019-02-25
116 | ### Changed
117 | - Cache proxy state for more performance
118 |
119 | ## [1.1.0] - 2019-02-17
120 | ### Changed
121 | - Improve useRef usage for concurrent mode
122 |
123 | ## [1.0.0] - 2019-02-09
124 | ### Changed
125 | - Improve initialization for concurrent mode
126 | - Updated dependencies (React 16.8)
127 |
128 | ## [0.10.0] - 2019-01-29
129 | ### Changed
130 | - Do not use useMemo as a semantic guarantee
131 |
132 | ## [0.9.0] - 2019-01-10
133 | ### Added
134 | - useReduxStateSimple for shallow object comparison
135 |
136 | ## [0.8.0] - 2018-12-24
137 | ### Changed
138 | - No spread guards in proxyequal for better compatibility
139 |
140 | ## [0.7.0] - 2018-12-19
141 | ### Added
142 | - Better warning message for no ReduxProvider
143 | ### Changed
144 | - Refactor to support dynamic updating
145 |
146 | ## [0.6.0] - 2018-12-17
147 | ### Changed
148 | - Support changing store
149 |
150 | ## [0.5.0] - 2018-12-13
151 | ### Changed
152 | - Fix types and examples for the previous change
153 |
154 | ## [0.4.0] - 2018-12-13
155 | ### Changed
156 | - Gave up bailOutHack and use subscriptions
157 |
158 | ## [0.3.0] - 2018-11-20
159 | ### Changed
160 | - bailOutHack with ErrorBoundary
161 |
162 | ## [0.2.0] - 2018-11-17
163 | ### Added
164 | - Use proxyequal for deep change detection
165 |
166 | ## [0.1.0] - 2018-11-15
167 | ### Added
168 | - Initial experimental release
169 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018-2020 Daishi Kato
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This project is no longer maintained.
2 | [react-tracked](https://react-tracked.js.org) works with react-redux
3 | and covers the use case of reactive-react-redux.
4 | Redux docs officially recommends [proxy-memoize](https://redux.js.org/usage/deriving-data-selectors#proxy-memoize) as a selector library,
5 | and it provides similar developer experience to that of reactive-react-redux.
6 | Both are good options.
7 |
8 | ---
9 |
10 | There are several projects related to this repo.
11 | Here's the index of those.
12 |
13 | - reactive-react-redux v5-alpha (this repo): This has an experimental react-redux binding with useMutableSource. It provides useTrackedState, which tracks the usage of state in render, and it's originally proposed in this repo.
14 | - [react-tracked](https://github.com/dai-shi/react-tracked): This project is to provide useTrackedState with React Context. v1.6 provides createTrackedSelector that will create useTrackedState from useSelector.
15 | - [react-redux #1503](https://github.com/reduxjs/react-redux/pull/1503): A pull request to add useTrackedState to the official react-redux library.
16 | - [proxy-memoize](https://github.com/dai-shi/proxy-memoize): This is another project which is not tied to React, but combined with useSelector, we get a similar functionality like useTrackedState.
17 |
18 | ---
19 |
20 | # reactive-react-redux
21 |
22 | [](https://github.com/dai-shi/reactive-react-redux/actions?query=workflow%3ACI)
23 | [](https://www.npmjs.com/package/reactive-react-redux)
24 | [](https://bundlephobia.com/result?p=reactive-react-redux)
25 | [](https://discord.gg/MrQdmzd)
26 |
27 | React Redux binding with React Hooks and Proxy
28 |
29 | > If you are looking for a non-Redux library, please visit [react-tracked](https://github.com/dai-shi/react-tracked) which has the same hooks API.
30 |
31 | ## Introduction
32 |
33 | This is a library to bind React and Redux with Hooks API.
34 | It has mostly the same API as the official
35 | [react-redux Hooks API](https://react-redux.js.org/api/hooks),
36 | so it can be used as a drop-in replacement
37 | if you are using only basic functionality.
38 |
39 | There are two major features in this library
40 | that are not in the official react-redux.
41 |
42 | ### 1. useTrackedState hook
43 |
44 | This library provides another hook `useTrackedState`
45 | which is a simpler API than already simple `useSelector`.
46 | It returns an entire state, but the library takes care of
47 | optimization of re-renders.
48 | Most likely, `useTrackedState` performs better than
49 | `useSelector` without perfectly tuned selectors.
50 |
51 | Technically, `useTrackedState` has no [stale props](https://react-redux.js.org/api/hooks#stale-props-and-zombie-children) issue.
52 |
53 | ### 2. useMutableSource without Context
54 |
55 | react-redux v7 has APIs around Context.
56 | This library is implemented with useMutableSource,
57 | and it patches the Redux store.
58 | APIs are provided without Context.
59 | It's up to developers to use Context based on them.
60 | Check out `./examples/11_todolist/src/context.ts`.
61 |
62 | There's another difference from react-redux v7.
63 | This library directly use useMutableSource, and requires
64 | useCallback for the selector in useSelector.
65 | [equalityFn](https://react-redux.js.org/api/hooks#equality-comparisons-and-updates) is not supported.
66 |
67 | ## How tracking works
68 |
69 | A hook `useTrackedState` returns an entire Redux state object with Proxy,
70 | and it keeps track of which properties of the object are used
71 | in render. When the state is updated, this hook checks
72 | whether used properties are changed.
73 | Only if it detects changes in the state,
74 | it triggers a component to re-render.
75 |
76 | ## Install
77 |
78 | ```bash
79 | npm install reactive-react-redux
80 | ```
81 |
82 | ## Usage (useTrackedState)
83 |
84 | ```javascript
85 | import React from 'react';
86 | import { createStore } from 'redux';
87 | import {
88 | patchStore,
89 | useTrackedState,
90 | } from 'reactive-react-redux';
91 |
92 | const initialState = {
93 | count: 0,
94 | text: 'hello',
95 | };
96 |
97 | const reducer = (state = initialState, action) => {
98 | switch (action.type) {
99 | case 'increment': return { ...state, count: state.count + 1 };
100 | case 'decrement': return { ...state, count: state.count - 1 };
101 | case 'setText': return { ...state, text: action.text };
102 | default: return state;
103 | }
104 | };
105 |
106 | const store = patchStore(createStore(reducer));
107 |
108 | const Counter = () => {
109 | const state = useTrackedState(store);
110 | const { dispatch } = store;
111 | return (
112 |
113 | {Math.random()}
114 |
115 | Count: {state.count}
116 |
117 |
118 |
119 |
120 | );
121 | };
122 |
123 | const TextBox = () => {
124 | const state = useTrackedState(store);
125 | const { dispatch } = store;
126 | return (
127 |
128 | {Math.random()}
129 |
130 | Text: {state.text}
131 | dispatch({ type: 'setText', text: event.target.value })} />
132 |
133 |
134 | );
135 | };
136 |
137 | const App = () => (
138 | <>
139 | Counter
140 |
141 |
142 | TextBox
143 |
144 |
145 | >
146 | );
147 | ```
148 |
149 | ## API
150 |
151 |
152 |
153 | ### patchStore
154 |
155 | patch Redux store for React
156 |
157 | #### Parameters
158 |
159 | - `store` **Store<State, Action>**
160 |
161 | #### Examples
162 |
163 | ```javascript
164 | import { createStore } from 'redux';
165 | import { patchStore } from 'reactive-react-redux';
166 |
167 | const reducer = ...;
168 | const store = patchStore(createStore(reducer));
169 | ```
170 |
171 | ### useTrackedState
172 |
173 | useTrackedState hook
174 |
175 | It return the Redux state wrapped by Proxy,
176 | and the state prperty access is tracked.
177 | It will only re-render if accessed properties are changed.
178 |
179 | #### Parameters
180 |
181 | - `patchedStore` **PatchedStore<State, Action>**
182 | - `opts` **Opts** (optional, default `{}`)
183 |
184 | #### Examples
185 |
186 | ```javascript
187 | import { useTrackedState } from 'reactive-react-redux';
188 |
189 | const Component = () => {
190 | const state = useTrackedState(store);
191 | ...
192 | };
193 | ```
194 |
195 | ### useSelector
196 |
197 | useSelector hook
198 |
199 | selector has to be stable. Either define it outside render
200 | or use useCallback if selector uses props.
201 |
202 | #### Parameters
203 |
204 | - `patchedStore` **PatchedStore<State, Action>**
205 | - `selector` **function (state: State): Selected**
206 |
207 | #### Examples
208 |
209 | ```javascript
210 | import { useCallback } from 'react';
211 | import { useSelector } from 'reactive-react-redux';
212 |
213 | const Component = ({ count }) => {
214 | const isBigger = useSelector(store, useCallack(state => state.count > count, [count]));
215 | ...
216 | };
217 | ```
218 |
219 | ### memo
220 |
221 | memo
222 |
223 | Using `React.memo` with tracked state is not compatible,
224 | because `React.memo` stops state access, thus no tracking occurs.
225 | This is a special memo to be used instead of `React.memo` with tracking support.
226 |
227 | #### Parameters
228 |
229 | - `Component` **any**
230 | - `areEqual` **any?**
231 |
232 | #### Examples
233 |
234 | ```javascript
235 | import { memo } from 'reactive-react-redux';
236 |
237 | const ChildComponent = memo(({ obj1, obj2 }) => {
238 | // ...
239 | });
240 | ```
241 |
242 | ## Recipes
243 |
244 | ### Context
245 |
246 | You can create Context based APIs like react-redux v7.
247 |
248 | ```typescript
249 | import { createContext, createElement, useContext } from 'react';
250 | import {
251 | PatchedStore,
252 | useSelector as useSelectorOrig,
253 | useTrackedState as useTrackedStateOrig,
254 | } from 'reactive-react-redux';
255 |
256 | export type State = ...;
257 |
258 | export type Action = ...;
259 |
260 | const Context = createContext(new Proxy({}, {
261 | get() { throw new Error('use Provider'); },
262 | }) as PatchedStore);
263 |
264 | export const Provider: React.FC<{ store: PatchedStore }> = ({
265 | store,
266 | children,
267 | }) => createElement(Context.Provider, { value: store }, children);
268 |
269 | export const useDispatch = () => useContext(Context).dispatch;
270 |
271 | export const useSelector = (
272 | selector: (state: State) => Selected,
273 | ) => useSelectorOrig(useContext(Context), selector);
274 |
275 | export const useTrackedState = () => useTrackedStateOrig(useContext(Context));
276 | ```
277 |
278 | ### useTrackedSelector
279 |
280 | You can create a selector hook with tracking support.
281 |
282 | ```javascript
283 | import { useTrackedState } from 'reactive-react-redux';
284 |
285 | export const useTrackedSelector = (patchedStore, selector) => selector(useTrackedState(patchedStore));
286 | ```
287 |
288 | Please refer [this issue](https://github.com/dai-shi/reactive-react-redux/issues/41) for more information.
289 |
290 | ### useTracked
291 |
292 | You can combine useTrackedState and useDispatch to
293 | make a hook that returns a tuple like `useReducer`.
294 |
295 | ```javascript
296 | import { useTrackedState, useDispatch } from 'reactive-react-redux';
297 |
298 | export const useTracked = (patchedStore) => {
299 | const state = useTrackedState(patchedStore);
300 | const dispatch = useDispatch(patchedStore);
301 | return useMemo(() => [state, dispatch], [state, dispatch]);
302 | };
303 | ```
304 |
305 | ## Caveats
306 |
307 | Proxy and state usage tracking may not work 100% as expected.
308 | There are some limitations and workarounds.
309 |
310 | ### Proxied states are referentially equal only in per-hook basis
311 |
312 | ```javascript
313 | const state1 = useTrackedState(patchedStore);
314 | const state2 = useTrackedState(patchedStore);
315 | // state1 and state2 is not referentially equal
316 | // even if the underlying redux state is referentially equal.
317 | ```
318 |
319 | You should use `useTrackedState` only once in a component.
320 |
321 | ### An object referential change doesn't trigger re-render if an property of the object is accessed in previous render
322 |
323 | ```javascript
324 | const state = useTrackedState(patchedStore);
325 | const { foo } = state;
326 | return ;
327 |
328 | const Child = React.memo(({ foo }) => {
329 | // ...
330 | };
331 | // if foo doesn't change, Child won't render, so foo.id is only marked as used.
332 | // it won't trigger Child to re-render even if foo is changed.
333 | ```
334 |
335 | You need to use a special `memo` provided by this library.
336 |
337 | ```javascript
338 | import { memo } from 'reactive-react-redux';
339 |
340 | const Child = memo(({ foo }) => {
341 | // ...
342 | };
343 | ```
344 |
345 | ### Proxied state might behave unexpectedly outside render
346 |
347 | Proxies are basically transparent, and it should behave like normal objects.
348 | However, there can be edge cases where it behaves unexpectedly.
349 | For example, if you console.log a proxied value,
350 | it will display a proxy wrapping an object.
351 | Notice, it will be kept tracking outside render,
352 | so any prorerty access will mark as used to trigger re-render on updates.
353 |
354 | useTrackedState will unwrap a Proxy before wrapping with a new Proxy,
355 | hence, it will work fine in usual use cases.
356 | There's only one known pitfall: If you wrap proxied state with your own Proxy
357 | outside the control of useTrackedState,
358 | it might lead memory leaks, because useTrackedState
359 | wouldn't know how to unwrap your own Proxy.
360 |
361 | To work around such edge cases, the first option is to use primitive values.
362 |
363 | ```javascript
364 | const state = useTrackedState(patchedStore);
365 | const dispatch = useUpdate(patchedStore);
366 | dispatch({ type: 'FOO', value: state.fooObj }); // Instead of using objects,
367 | dispatch({ type: 'FOO', value: state.fooStr }); // Use primitives.
368 | ```
369 |
370 | The second option is to use `getUntrackedObject`.
371 |
372 | ```javascript
373 | import { getUntrackedObject } from 'react-tracked';
374 | dispatch({ type: 'FOO', value: getUntrackedObject(state.fooObj) });
375 | ```
376 |
377 | You could implement a special dispatch function to do this automatically.
378 |
379 | ## Examples
380 |
381 | The [examples](examples) folder contains working examples.
382 | You can run one of them with
383 |
384 | ```bash
385 | PORT=8080 npm run examples:01_minimal
386 | ```
387 |
388 | and open in your web browser.
389 |
390 | You can also try them in codesandbox.io:
391 | [01](https://codesandbox.io/s/github/dai-shi/reactive-react-redux/tree/master/examples/01_minimal)
392 | [02](https://codesandbox.io/s/github/dai-shi/reactive-react-redux/tree/master/examples/02_typescript)
393 | [03](https://codesandbox.io/s/github/dai-shi/reactive-react-redux/tree/master/examples/03_deep)
394 | [04](https://codesandbox.io/s/github/dai-shi/reactive-react-redux/tree/master/examples/04_immer)
395 | [05](https://codesandbox.io/s/github/dai-shi/reactive-react-redux/tree/master/examples/05_localstate)
396 | [06](https://codesandbox.io/s/github/dai-shi/reactive-react-redux/tree/master/examples/06_memoization)
397 | [07](https://codesandbox.io/s/github/dai-shi/reactive-react-redux/tree/master/examples/07_multistore)
398 | [08](https://codesandbox.io/s/github/dai-shi/reactive-react-redux/tree/master/examples/08_dynamic)
399 | [09](https://codesandbox.io/s/github/dai-shi/reactive-react-redux/tree/master/examples/09_thunk)
400 | [11](https://codesandbox.io/s/github/dai-shi/reactive-react-redux/tree/master/examples/11_todolist)
401 | [12](https://codesandbox.io/s/github/dai-shi/reactive-react-redux/tree/master/examples/12_async)
402 | [13](https://codesandbox.io/s/github/dai-shi/reactive-react-redux/tree/master/examples/13_memo)
403 |
404 | ## Benchmarks
405 |
406 |
407 |
408 | See [#32](https://github.com/dai-shi/reactive-react-redux/issues/32) for details.
409 |
410 | ## Blogs
411 |
412 | - [A deadly simple React bindings library for Redux with Hooks API](https://blog.axlight.com/posts/a-deadly-simple-react-bindings-library-for-redux-with-hooks-api/)
413 | - [Developing React custom hooks for Redux without react-redux](https://blog.axlight.com/posts/developing-react-custom-hooks-for-redux-without-react-redux/)
414 | - [Integrating React and Redux, with Hooks and Proxies](https://frontarm.com/daishi-kato/redux-custom-hooks/)
415 | - [New React Redux coding style with hooks without selectors](https://blog.axlight.com/posts/new-react-redux-coding-style-with-hooks-without-selectors/)
416 | - [Benchmark alpha-released hooks API in React Redux with alternatives](https://blog.axlight.com/posts/benchmark-alpha-released-hooks-api-in-react-redux-with-alternatives/)
417 | - [Four patterns for global state with React hooks: Context or Redux](https://blog.axlight.com/posts/four-patterns-for-global-state-with-react-hooks-context-or-redux/)
418 | - [Redux meets hooks for non-redux users: a small concrete example with reactive-react-redux](https://blog.axlight.com/posts/redux-meets-hooks-for-non-redux-users-a-small-concrete-example-with-reactive-react-redux/)
419 | - [Redux-less context-based useSelector hook that has same performance as React-Redux](https://blog.axlight.com/posts/benchmark-react-tracked/)
420 | - [What is state usage tracking? A novel approach to intuitive and performant global state with React hooks and Proxy](https://blog.axlight.com/posts/what-is-state-usage-tracking-a-novel-approach-to-intuitive-and-performant-api-with-react-hooks-and-proxy/)
421 | - [Effortless render optimization with state usage tracking with React hooks](https://blog.axlight.com/posts/effortless-render-optimization-with-state-usage-tracking-with-react-hooks/)
422 | - [How I developed a Concurrent Mode friendly library for React Redux](https://blog.axlight.com/posts/how-i-developed-a-concurrent-mode-friendly-library-for-react-redux/)
423 | - [React hooks-oriented Redux coding pattern without thunks and action creators](https://blog.axlight.com/posts/react-hooks-oriented-redux-coding-pattern-without-thunks-and-action-creators/)
424 |
--------------------------------------------------------------------------------
/__tests__/01_basic_spec.tsx:
--------------------------------------------------------------------------------
1 | import React, { StrictMode } from 'react';
2 | import { AnyAction, createStore } from 'redux';
3 |
4 | import { render, fireEvent, cleanup } from '@testing-library/react';
5 |
6 | import {
7 | patchStore,
8 | useSelector,
9 | useTrackedState,
10 | } from '../src/index';
11 |
12 | describe('basic spec', () => {
13 | afterEach(cleanup);
14 |
15 | it('hooks are defiend', () => {
16 | expect(useSelector).toBeDefined();
17 | expect(useTrackedState).toBeDefined();
18 | });
19 |
20 | it('create a component', () => {
21 | const initialState = {
22 | count1: 0,
23 | };
24 | type State = typeof initialState;
25 | const reducer = (state = initialState, action: AnyAction) => {
26 | if (action.type === 'increment') {
27 | return { ...state, count1: state.count1 + 1 };
28 | }
29 | return state;
30 | };
31 | const store = patchStore(createStore(reducer));
32 | const Counter = () => {
33 | const value = useTrackedState(store);
34 | const { dispatch } = store;
35 | return (
36 |
37 | {value.count1}
38 |
39 |
40 | );
41 | };
42 | const App = () => (
43 |
44 | <>
45 |
46 |
47 | >
48 |
49 | );
50 | const { getAllByText, container } = render();
51 | expect(container).toMatchSnapshot();
52 | fireEvent.click(getAllByText('+1')[0]);
53 | expect(container).toMatchSnapshot();
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/__tests__/__snapshots__/01_basic_spec.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`basic spec create a component 1`] = `
4 |
5 |
6 |
7 | 0
8 |
9 |
14 |
15 |
16 |
17 | 0
18 |
19 |
24 |
25 |
26 | `;
27 |
28 | exports[`basic spec create a component 2`] = `
29 |
30 |
31 |
32 | 1
33 |
34 |
39 |
40 |
41 |
42 | 1
43 |
44 |
49 |
50 |
51 | `;
52 |
--------------------------------------------------------------------------------
/examples/01_minimal/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reactive-react-redux-example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "experimental",
7 | "react-dom": "experimental",
8 | "reactive-react-redux": "latest",
9 | "react-scripts": "latest",
10 | "redux": "latest"
11 | },
12 | "scripts": {
13 | "start": "react-scripts start",
14 | "build": "react-scripts build",
15 | "test": "react-scripts test",
16 | "eject": "react-scripts eject"
17 | },
18 | "browserslist": [
19 | ">0.2%",
20 | "not dead",
21 | "not ie <= 11",
22 | "not op_mini all"
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/examples/01_minimal/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | reactive-react-redux example
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/examples/01_minimal/src/index.js:
--------------------------------------------------------------------------------
1 | import React, { StrictMode } from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { createStore } from 'redux';
4 |
5 | import {
6 | patchStore,
7 | useTrackedState,
8 | } from 'reactive-react-redux';
9 |
10 | const initialState = {
11 | count: 0,
12 | text: 'hello',
13 | };
14 |
15 | const reducer = (state = initialState, action) => {
16 | switch (action.type) {
17 | case 'increment': return { ...state, count: state.count + 1 };
18 | case 'decrement': return { ...state, count: state.count - 1 };
19 | case 'setText': return { ...state, text: action.text };
20 | default: return state;
21 | }
22 | };
23 |
24 | const store = patchStore(createStore(reducer));
25 |
26 | const Counter = () => {
27 | const state = useTrackedState(store);
28 | const { dispatch } = store;
29 | return (
30 |
31 | {Math.random()}
32 |
33 | Count: {state.count}
34 |
35 |
36 |
37 |
38 | );
39 | };
40 |
41 | const TextBox = () => {
42 | const state = useTrackedState(store);
43 | const { dispatch } = store;
44 | return (
45 |
46 | {Math.random()}
47 |
48 | Text: {state.text}
49 | dispatch({ type: 'setText', text: event.target.value })} />
50 |
51 |
52 | );
53 | };
54 |
55 | const App = () => (
56 |
57 | <>
58 | Counter
59 |
60 |
61 | TextBox
62 |
63 |
64 | >
65 |
66 | );
67 |
68 | ReactDOM.unstable_createRoot(document.getElementById('app')).render();
69 |
--------------------------------------------------------------------------------
/examples/02_typescript/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reactive-react-redux-example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@types/react": "latest",
7 | "@types/react-dom": "latest",
8 | "react": "experimental",
9 | "react-dom": "experimental",
10 | "reactive-react-redux": "latest",
11 | "react-scripts": "latest",
12 | "redux": "latest",
13 | "typescript": "latest"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test",
19 | "eject": "react-scripts eject"
20 | },
21 | "browserslist": [
22 | ">0.2%",
23 | "not dead",
24 | "not ie <= 11",
25 | "not op_mini all"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/examples/02_typescript/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | reactive-react-redux example
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/examples/02_typescript/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { StrictMode } from 'react';
2 | import { createStore } from 'redux';
3 |
4 | import { reducer } from './state';
5 | import { Provider } from './context';
6 |
7 | import Counter from './Counter';
8 | import Person from './Person';
9 |
10 | const store = createStore(reducer);
11 |
12 | const App = () => (
13 |
14 |
15 | Counter
16 |
17 |
18 | Person
19 |
20 |
21 |
22 |
23 | );
24 |
25 | export default App;
26 |
--------------------------------------------------------------------------------
/examples/02_typescript/src/Counter.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { useDispatch, useTrackedState } from './context';
4 |
5 | const Counter = () => {
6 | const state = useTrackedState();
7 | const dispatch = useDispatch();
8 | return (
9 |
10 | {Math.random()}
11 |
12 | Count: {state.count}
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default Counter;
21 |
--------------------------------------------------------------------------------
/examples/02_typescript/src/Person.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { useDispatch, useTrackedState } from './context';
4 |
5 | const Counter: React.FC<{ firstName: string }> = ({ firstName }) => {
6 | const state = useTrackedState();
7 | const dispatch = useDispatch();
8 | return (
9 |
10 | {Math.random()}
11 | {firstName}
12 |
13 | Count: {state.count}
14 |
15 |
16 |
17 |
18 | );
19 | };
20 |
21 | const Person = () => {
22 | const state = useTrackedState();
23 | const dispatch = useDispatch();
24 | return (
25 |
59 | );
60 | };
61 |
62 | export default Person;
63 |
--------------------------------------------------------------------------------
/examples/02_typescript/src/context.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createContext,
3 | createElement,
4 | useContext,
5 | useMemo,
6 | } from 'react';
7 | import { Store } from 'redux';
8 | import {
9 | PatchedStore,
10 | patchStore,
11 | useSelector as useSelectorOrig,
12 | useTrackedState as useTrackedStateOrig,
13 | } from 'reactive-react-redux';
14 |
15 | import { State, Action } from './state';
16 |
17 | // Context based APIs
18 |
19 | const Context = createContext(new Proxy({}, {
20 | get() { throw new Error('use Provider'); },
21 | }) as PatchedStore);
22 |
23 | export const Provider: React.FC<{ store: Store }> = ({
24 | store,
25 | children,
26 | }) => {
27 | const value = useMemo(() => patchStore(store), [store]);
28 | return createElement(Context.Provider, { value }, children);
29 | };
30 |
31 | export const useDispatch = () => useContext(Context).dispatch;
32 |
33 | export const useSelector = (
34 | selector: (state: State) => Selected,
35 | ) => useSelectorOrig(useContext(Context), selector);
36 |
37 | export const useTrackedState = () => useTrackedStateOrig(useContext(Context));
38 |
--------------------------------------------------------------------------------
/examples/02_typescript/src/index.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line spaced-comment
2 | ///
3 |
4 | import React from 'react';
5 | import { unstable_createRoot as createRoot } from 'react-dom';
6 |
7 | import App from './App';
8 |
9 | const ele = document.getElementById('app');
10 | if (!ele) throw new Error('no app');
11 | createRoot(ele).render(React.createElement(App));
12 |
--------------------------------------------------------------------------------
/examples/02_typescript/src/state.ts:
--------------------------------------------------------------------------------
1 | const initialState = {
2 | count: 0,
3 | person: {
4 | age: 0,
5 | firstName: '',
6 | lastName: '',
7 | },
8 | };
9 |
10 | export type State = typeof initialState;
11 |
12 | export type Action =
13 | | { type: 'increment' }
14 | | { type: 'decrement' }
15 | | { type: 'setFirstName'; firstName: string }
16 | | { type: 'setLastName'; lastName: string }
17 | | { type: 'setAge'; age: number };
18 |
19 | export const reducer = (state = initialState, action: Action) => {
20 | switch (action.type) {
21 | case 'increment': return {
22 | ...state,
23 | count: state.count + 1,
24 | };
25 | case 'decrement': return {
26 | ...state,
27 | count: state.count - 1,
28 | };
29 | case 'setFirstName': return {
30 | ...state,
31 | person: {
32 | ...state.person,
33 | firstName: action.firstName,
34 | },
35 | };
36 | case 'setLastName': return {
37 | ...state,
38 | person: {
39 | ...state.person,
40 | lastName: action.lastName,
41 | },
42 | };
43 | case 'setAge': return {
44 | ...state,
45 | person: {
46 | ...state.person,
47 | age: action.age,
48 | },
49 | };
50 | default: return state;
51 | }
52 | };
53 |
--------------------------------------------------------------------------------
/examples/03_deep/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reactive-react-redux-example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@types/react": "latest",
7 | "@types/react-dom": "latest",
8 | "react": "experimental",
9 | "react-dom": "experimental",
10 | "reactive-react-redux": "latest",
11 | "react-scripts": "latest",
12 | "redux": "latest",
13 | "typescript": "latest"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test",
19 | "eject": "react-scripts eject"
20 | },
21 | "browserslist": [
22 | ">0.2%",
23 | "not dead",
24 | "not ie <= 11",
25 | "not op_mini all"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/examples/03_deep/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | reactive-react-redux example
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/examples/03_deep/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { StrictMode } from 'react';
2 | import { createStore } from 'redux';
3 |
4 | import { reducer } from './state';
5 | import { Provider } from './context';
6 |
7 | import Counter from './Counter';
8 | import Person from './Person';
9 |
10 | const store = createStore(reducer);
11 |
12 | const App = () => (
13 |
14 |
15 | Counter
16 |
17 |
18 | Person
19 |
20 |
21 |
22 |
23 | );
24 |
25 | export default App;
26 |
--------------------------------------------------------------------------------
/examples/03_deep/src/Counter.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { useDispatch, useTrackedState } from './context';
4 |
5 | const Counter = () => {
6 | const state = useTrackedState();
7 | const dispatch = useDispatch();
8 | return (
9 |
10 | {Math.random()}
11 |
12 | Count: {state.count}
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default Counter;
21 |
--------------------------------------------------------------------------------
/examples/03_deep/src/Person.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { useDispatch, useTrackedState } from './context';
4 |
5 | const TextBox: React.SFC<{ text: string }> = ({ text }) => {
6 | // eslint-disable-next-line no-console
7 | console.log('rendering text:', text);
8 | return {text};
9 | };
10 |
11 | const PersonFirstName = () => {
12 | const state = useTrackedState();
13 | const dispatch = useDispatch();
14 | return (
15 |
16 | First Name:
17 |
18 | {
21 | const firstName = event.target.value;
22 | dispatch({ firstName, type: 'setFirstName' });
23 | }}
24 | />
25 |
26 | );
27 | };
28 |
29 | const PersonLastName = () => {
30 | const state = useTrackedState();
31 | const dispatch = useDispatch();
32 | return (
33 |
34 | Last Name:
35 |
36 | {
39 | const lastName = event.target.value;
40 | dispatch({ lastName, type: 'setLastName' });
41 | }}
42 | />
43 |
44 | );
45 | };
46 |
47 | const PersonAge = () => {
48 | const state = useTrackedState();
49 | const dispatch = useDispatch();
50 | return (
51 |
52 | Age:
53 | {
56 | const age = Number(event.target.value) || 0;
57 | dispatch({ age, type: 'setAge' });
58 | }}
59 | />
60 |
61 | );
62 | };
63 |
64 | const Person = () => (
65 | <>
66 |
67 |
68 |
69 | >
70 | );
71 |
72 | export default Person;
73 |
--------------------------------------------------------------------------------
/examples/03_deep/src/context.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createContext,
3 | createElement,
4 | useContext,
5 | useMemo,
6 | } from 'react';
7 | import { Store } from 'redux';
8 | import {
9 | PatchedStore,
10 | patchStore,
11 | useSelector as useSelectorOrig,
12 | useTrackedState as useTrackedStateOrig,
13 | } from 'reactive-react-redux';
14 |
15 | import { State, Action } from './state';
16 |
17 | // Context based APIs
18 |
19 | const Context = createContext(new Proxy({}, {
20 | get() { throw new Error('use Provider'); },
21 | }) as PatchedStore);
22 |
23 | export const Provider: React.FC<{ store: Store }> = ({
24 | store,
25 | children,
26 | }) => {
27 | const value = useMemo(() => patchStore(store), [store]);
28 | return createElement(Context.Provider, { value }, children);
29 | };
30 |
31 | export const useDispatch = () => useContext(Context).dispatch;
32 |
33 | export const useSelector = (
34 | selector: (state: State) => Selected,
35 | ) => useSelectorOrig(useContext(Context), selector);
36 |
37 | export const useTrackedState = () => useTrackedStateOrig(useContext(Context));
38 |
--------------------------------------------------------------------------------
/examples/03_deep/src/index.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line spaced-comment
2 | ///
3 |
4 | import React from 'react';
5 | import { unstable_createRoot as createRoot } from 'react-dom';
6 |
7 | import App from './App';
8 |
9 | const ele = document.getElementById('app');
10 | if (!ele) throw new Error('no app');
11 | createRoot(ele).render(React.createElement(App));
12 |
--------------------------------------------------------------------------------
/examples/03_deep/src/state.ts:
--------------------------------------------------------------------------------
1 | const initialState = {
2 | count: 0,
3 | person: {
4 | age: 0,
5 | firstName: '',
6 | lastName: '',
7 | },
8 | };
9 |
10 | export type State = typeof initialState;
11 |
12 | export type Action =
13 | | { type: 'increment' }
14 | | { type: 'decrement' }
15 | | { type: 'setFirstName'; firstName: string }
16 | | { type: 'setLastName'; lastName: string }
17 | | { type: 'setAge'; age: number };
18 |
19 | export const reducer = (state = initialState, action: Action) => {
20 | switch (action.type) {
21 | case 'increment': return {
22 | ...state,
23 | count: state.count + 1,
24 | };
25 | case 'decrement': return {
26 | ...state,
27 | count: state.count - 1,
28 | };
29 | case 'setFirstName': return {
30 | ...state,
31 | person: {
32 | ...state.person,
33 | firstName: action.firstName,
34 | },
35 | };
36 | case 'setLastName': return {
37 | ...state,
38 | person: {
39 | ...state.person,
40 | lastName: action.lastName,
41 | },
42 | };
43 | case 'setAge': return {
44 | ...state,
45 | person: {
46 | ...state.person,
47 | age: action.age,
48 | },
49 | };
50 | default: return state;
51 | }
52 | };
53 |
--------------------------------------------------------------------------------
/examples/04_immer/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reactive-react-redux-example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@types/react": "latest",
7 | "@types/react-dom": "latest",
8 | "immer": "latest",
9 | "react": "experimental",
10 | "react-dom": "experimental",
11 | "reactive-react-redux": "latest",
12 | "react-scripts": "latest",
13 | "redux": "latest",
14 | "typescript": "latest"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "test": "react-scripts test",
20 | "eject": "react-scripts eject"
21 | },
22 | "browserslist": [
23 | ">0.2%",
24 | "not dead",
25 | "not ie <= 11",
26 | "not op_mini all"
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/examples/04_immer/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | reactive-react-redux example
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/examples/04_immer/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { StrictMode } from 'react';
2 | import { createStore } from 'redux';
3 |
4 | import { reducer } from './state';
5 | import { Provider } from './context';
6 |
7 | import Counter from './Counter';
8 | import Person from './Person';
9 |
10 | const store = createStore(reducer);
11 |
12 | const App = () => (
13 |
14 |
15 | Counter
16 |
17 |
18 | Person
19 |
20 |
21 |
22 |
23 | );
24 |
25 | export default App;
26 |
--------------------------------------------------------------------------------
/examples/04_immer/src/Counter.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { useDispatch, useTrackedState } from './context';
4 |
5 | const Counter = () => {
6 | const state = useTrackedState();
7 | const dispatch = useDispatch();
8 | return (
9 |
10 | {Math.random()}
11 |
12 | Count: {state.count}
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default Counter;
21 |
--------------------------------------------------------------------------------
/examples/04_immer/src/Person.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { useDispatch, useTrackedState } from './context';
4 |
5 | const TextBox: React.SFC<{ text: string }> = ({ text }) => {
6 | // eslint-disable-next-line no-console
7 | console.log('rendering text:', text);
8 | return {text};
9 | };
10 |
11 | const PersonFirstName = () => {
12 | const state = useTrackedState();
13 | const dispatch = useDispatch();
14 | return (
15 |
16 | First Name:
17 |
18 | {
21 | const firstName = event.target.value;
22 | dispatch({ firstName, type: 'setFirstName' });
23 | }}
24 | />
25 |
26 | );
27 | };
28 |
29 | const PersonLastName = () => {
30 | const state = useTrackedState();
31 | const dispatch = useDispatch();
32 | return (
33 |
34 | Last Name:
35 |
36 | {
39 | const lastName = event.target.value;
40 | dispatch({ lastName, type: 'setLastName' });
41 | }}
42 | />
43 |
44 | );
45 | };
46 |
47 | const PersonAge = () => {
48 | const state = useTrackedState();
49 | const dispatch = useDispatch();
50 | return (
51 |
52 | Age:
53 | {
56 | const age = Number(event.target.value) || 0;
57 | dispatch({ age, type: 'setAge' });
58 | }}
59 | />
60 |
61 | );
62 | };
63 |
64 | const Person = () => (
65 | <>
66 |
67 |
68 |
69 | >
70 | );
71 |
72 | export default Person;
73 |
--------------------------------------------------------------------------------
/examples/04_immer/src/context.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createContext,
3 | createElement,
4 | useContext,
5 | useMemo,
6 | } from 'react';
7 | import { Store } from 'redux';
8 | import {
9 | PatchedStore,
10 | patchStore,
11 | useSelector as useSelectorOrig,
12 | useTrackedState as useTrackedStateOrig,
13 | } from 'reactive-react-redux';
14 |
15 | import { State, Action } from './state';
16 |
17 | // Context based APIs
18 |
19 | const Context = createContext(new Proxy({}, {
20 | get() { throw new Error('use Provider'); },
21 | }) as PatchedStore);
22 |
23 | export const Provider: React.FC<{ store: Store }> = ({
24 | store,
25 | children,
26 | }) => {
27 | const value = useMemo(() => patchStore(store), [store]);
28 | return createElement(Context.Provider, { value }, children);
29 | };
30 |
31 | export const useDispatch = () => useContext(Context).dispatch;
32 |
33 | export const useSelector = (
34 | selector: (state: State) => Selected,
35 | ) => useSelectorOrig(useContext(Context), selector);
36 |
37 | export const useTrackedState = () => useTrackedStateOrig(useContext(Context));
38 |
--------------------------------------------------------------------------------
/examples/04_immer/src/index.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line spaced-comment
2 | ///
3 |
4 | import React from 'react';
5 | import { unstable_createRoot as createRoot } from 'react-dom';
6 |
7 | import App from './App';
8 |
9 | const ele = document.getElementById('app');
10 | if (!ele) throw new Error('no app');
11 | createRoot(ele).render(React.createElement(App));
12 |
--------------------------------------------------------------------------------
/examples/04_immer/src/state.ts:
--------------------------------------------------------------------------------
1 | import { produce } from 'immer';
2 |
3 | const initialState = {
4 | count: 0,
5 | person: {
6 | age: 0,
7 | firstName: '',
8 | lastName: '',
9 | },
10 | };
11 |
12 | export type State = typeof initialState;
13 |
14 | export type Action =
15 | | { type: 'increment' }
16 | | { type: 'decrement' }
17 | | { type: 'setFirstName'; firstName: string }
18 | | { type: 'setLastName'; lastName: string }
19 | | { type: 'setAge'; age: number };
20 |
21 | export const reducer = (state = initialState, action: Action) => produce(state, (draft) => {
22 | switch (action.type) {
23 | case 'increment': draft.count += 1; break;
24 | case 'decrement': draft.count -= 1; break;
25 | case 'setFirstName': draft.person.firstName = action.firstName; break;
26 | case 'setLastName': draft.person.lastName = action.lastName; break;
27 | case 'setAge': draft.person.age = action.age; break;
28 | }
29 | });
30 |
--------------------------------------------------------------------------------
/examples/05_localstate/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reactive-react-redux-example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@types/react": "latest",
7 | "@types/react-dom": "latest",
8 | "react": "experimental",
9 | "react-dom": "experimental",
10 | "reactive-react-redux": "latest",
11 | "react-scripts": "latest",
12 | "redux": "latest",
13 | "typescript": "latest"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test",
19 | "eject": "react-scripts eject"
20 | },
21 | "browserslist": [
22 | ">0.2%",
23 | "not dead",
24 | "not ie <= 11",
25 | "not op_mini all"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/examples/05_localstate/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | reactive-react-redux example
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/examples/05_localstate/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { StrictMode } from 'react';
2 | import { createStore } from 'redux';
3 |
4 | import { reducer } from './state';
5 | import { Provider } from './context';
6 |
7 | import Counter from './Counter';
8 |
9 | const store = createStore(reducer);
10 |
11 | const App = () => (
12 |
13 |
14 | Counter
15 |
16 | Counter
17 |
18 |
19 |
20 | );
21 |
22 | export default App;
23 |
--------------------------------------------------------------------------------
/examples/05_localstate/src/Counter.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { useDispatch, useTrackedState } from './context';
4 |
5 | const Counter = () => {
6 | const [count, setCount] = useState(0);
7 | const state = useTrackedState();
8 | const dispatch = useDispatch();
9 | return (
10 |
11 |
12 | Local count: {count}
13 |
14 |
15 |
16 |
17 | Global count: {state.count}
18 |
19 |
20 |
21 |
22 | );
23 | };
24 |
25 | export default Counter;
26 |
--------------------------------------------------------------------------------
/examples/05_localstate/src/context.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createContext,
3 | createElement,
4 | useContext,
5 | useMemo,
6 | } from 'react';
7 | import { Store } from 'redux';
8 | import {
9 | PatchedStore,
10 | patchStore,
11 | useSelector as useSelectorOrig,
12 | useTrackedState as useTrackedStateOrig,
13 | } from 'reactive-react-redux';
14 |
15 | import { State, Action } from './state';
16 |
17 | // Context based APIs
18 |
19 | const Context = createContext(new Proxy({}, {
20 | get() { throw new Error('use Provider'); },
21 | }) as PatchedStore);
22 |
23 | export const Provider: React.FC<{ store: Store }> = ({
24 | store,
25 | children,
26 | }) => {
27 | const value = useMemo(() => patchStore(store), [store]);
28 | return createElement(Context.Provider, { value }, children);
29 | };
30 |
31 | export const useDispatch = () => useContext(Context).dispatch;
32 |
33 | export const useSelector = (
34 | selector: (state: State) => Selected,
35 | ) => useSelectorOrig(useContext(Context), selector);
36 |
37 | export const useTrackedState = () => useTrackedStateOrig(useContext(Context));
38 |
--------------------------------------------------------------------------------
/examples/05_localstate/src/index.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line spaced-comment
2 | ///
3 |
4 | import React from 'react';
5 | import { unstable_createRoot as createRoot } from 'react-dom';
6 |
7 | import App from './App';
8 |
9 | const ele = document.getElementById('app');
10 | if (!ele) throw new Error('no app');
11 | createRoot(ele).render(React.createElement(App));
12 |
--------------------------------------------------------------------------------
/examples/05_localstate/src/state.ts:
--------------------------------------------------------------------------------
1 | const initialState = {
2 | count: 0,
3 | };
4 |
5 | export type State = typeof initialState;
6 |
7 | export type Action =
8 | | { type: 'increment' }
9 | | { type: 'decrement' };
10 |
11 | export const reducer = (state = initialState, action: Action) => {
12 | switch (action.type) {
13 | case 'increment': return {
14 | ...state,
15 | count: state.count + 1,
16 | };
17 | case 'decrement': return {
18 | ...state,
19 | count: state.count - 1,
20 | };
21 | default: return state;
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/examples/06_memoization/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reactive-react-redux-example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@types/react": "latest",
7 | "@types/react-dom": "latest",
8 | "react": "experimental",
9 | "react-dom": "experimental",
10 | "reactive-react-redux": "latest",
11 | "react-scripts": "latest",
12 | "redux": "latest",
13 | "typescript": "latest"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test",
19 | "eject": "react-scripts eject"
20 | },
21 | "browserslist": [
22 | ">0.2%",
23 | "not dead",
24 | "not ie <= 11",
25 | "not op_mini all"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/examples/06_memoization/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | reactive-react-redux example
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/examples/06_memoization/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { StrictMode } from 'react';
2 | import { createStore } from 'redux';
3 |
4 | import { reducer } from './state';
5 | import { Provider } from './context';
6 |
7 | import Counter from './Counter';
8 | import Person from './Person';
9 |
10 | const store = createStore(reducer);
11 |
12 | const App = () => (
13 |
14 |
15 | Counter
16 |
17 |
18 | Person
19 |
20 |
21 |
22 |
23 | );
24 |
25 | export default App;
26 |
--------------------------------------------------------------------------------
/examples/06_memoization/src/Counter.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { useDispatch, useTrackedState } from './context';
4 |
5 | const Counter = () => {
6 | const state = useTrackedState();
7 | const dispatch = useDispatch();
8 | return (
9 |
10 | {Math.random()}
11 |
12 | Count: {state.count}
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default Counter;
21 |
--------------------------------------------------------------------------------
/examples/06_memoization/src/Person.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react';
2 |
3 | import { useDispatch, useTrackedState } from './context';
4 |
5 | const TextBox: React.SFC<{ text: string }> = ({ text }) => {
6 | // eslint-disable-next-line no-console
7 | console.log('rendering text:', text);
8 | return {text};
9 | };
10 |
11 | const Person = () => {
12 | const state = useTrackedState();
13 | const dispatch = useDispatch();
14 | const { age, firstName, lastName } = useMemo(
15 | () => ({
16 | age: state.person.age,
17 | firstName: state.person.name.firstName,
18 | lastName: state.person.name.lastName,
19 | }),
20 | [state.person],
21 | );
22 | return (
23 |
57 | );
58 | };
59 |
60 | export default Person;
61 |
--------------------------------------------------------------------------------
/examples/06_memoization/src/context.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createContext,
3 | createElement,
4 | useContext,
5 | useMemo,
6 | } from 'react';
7 | import { Store } from 'redux';
8 | import {
9 | PatchedStore,
10 | patchStore,
11 | useSelector as useSelectorOrig,
12 | useTrackedState as useTrackedStateOrig,
13 | } from 'reactive-react-redux';
14 |
15 | import { State, Action } from './state';
16 |
17 | // Context based APIs
18 |
19 | const Context = createContext(new Proxy({}, {
20 | get() { throw new Error('use Provider'); },
21 | }) as PatchedStore);
22 |
23 | export const Provider: React.FC<{ store: Store }> = ({
24 | store,
25 | children,
26 | }) => {
27 | const value = useMemo(() => patchStore(store), [store]);
28 | return createElement(Context.Provider, { value }, children);
29 | };
30 |
31 | export const useDispatch = () => useContext(Context).dispatch;
32 |
33 | export const useSelector = (
34 | selector: (state: State) => Selected,
35 | ) => useSelectorOrig(useContext(Context), selector);
36 |
37 | export const useTrackedState = () => useTrackedStateOrig(useContext(Context));
38 |
--------------------------------------------------------------------------------
/examples/06_memoization/src/index.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line spaced-comment
2 | ///
3 |
4 | import React from 'react';
5 | import { unstable_createRoot as createRoot } from 'react-dom';
6 |
7 | import App from './App';
8 |
9 | const ele = document.getElementById('app');
10 | if (!ele) throw new Error('no app');
11 | createRoot(ele).render(React.createElement(App));
12 |
--------------------------------------------------------------------------------
/examples/06_memoization/src/state.ts:
--------------------------------------------------------------------------------
1 | const initialState = {
2 | count: 0,
3 | person: {
4 | age: 0,
5 | name: {
6 | firstName: '',
7 | lastName: '',
8 | },
9 | },
10 | };
11 |
12 | export type State = typeof initialState;
13 |
14 | export type Action =
15 | | { type: 'increment' }
16 | | { type: 'decrement' }
17 | | { type: 'setFirstName'; firstName: string }
18 | | { type: 'setLastName'; lastName: string }
19 | | { type: 'setAge'; age: number };
20 |
21 | export const reducer = (state = initialState, action: Action) => {
22 | switch (action.type) {
23 | case 'increment': return {
24 | ...state,
25 | count: state.count + 1,
26 | };
27 | case 'decrement': return {
28 | ...state,
29 | count: state.count - 1,
30 | };
31 | case 'setFirstName': return {
32 | ...state,
33 | person: {
34 | ...state.person,
35 | name: {
36 | ...state.person.name,
37 | firstName: action.firstName,
38 | },
39 | },
40 | };
41 | case 'setLastName': return {
42 | ...state,
43 | person: {
44 | ...state.person,
45 | name: {
46 | ...state.person.name,
47 | lastName: action.lastName,
48 | },
49 | },
50 | };
51 | case 'setAge': return {
52 | ...state,
53 | person: {
54 | ...state.person,
55 | age: action.age,
56 | },
57 | };
58 | default: return state;
59 | }
60 | };
61 |
--------------------------------------------------------------------------------
/examples/07_multistore/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reactive-react-redux-example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@types/react": "latest",
7 | "@types/react-dom": "latest",
8 | "react": "experimental",
9 | "react-dom": "experimental",
10 | "reactive-react-redux": "latest",
11 | "react-scripts": "latest",
12 | "redux": "latest",
13 | "typescript": "latest"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test",
19 | "eject": "react-scripts eject"
20 | },
21 | "browserslist": [
22 | ">0.2%",
23 | "not dead",
24 | "not ie <= 11",
25 | "not op_mini all"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/examples/07_multistore/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | reactive-react-redux example
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/examples/07_multistore/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, StrictMode } from 'react';
2 | import { createStore } from 'redux';
3 |
4 | import { reducer } from './state';
5 | import { Provider } from './context';
6 |
7 | import Counter from './Counter';
8 | import Person from './Person';
9 |
10 | const store1 = createStore(reducer);
11 | const store2 = createStore(reducer);
12 |
13 | const App = () => {
14 | const [store, setStore] = useState(store1);
15 | return (
16 |
17 |
18 |
19 |
20 |
21 | Counter
22 |
23 |
24 | Person
25 |
26 |
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | export default App;
34 |
--------------------------------------------------------------------------------
/examples/07_multistore/src/Counter.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { useDispatch, useTrackedState } from './context';
4 |
5 | const Counter = () => {
6 | const state = useTrackedState();
7 | const dispatch = useDispatch();
8 | return (
9 |
10 |
11 | Count: {state.count}
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default Counter;
20 |
--------------------------------------------------------------------------------
/examples/07_multistore/src/Person.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { useDispatch, useTrackedState } from './context';
4 |
5 | const Person = () => {
6 | const state = useTrackedState();
7 | const dispatch = useDispatch();
8 | return (
9 |
41 | );
42 | };
43 |
44 | export default Person;
45 |
--------------------------------------------------------------------------------
/examples/07_multistore/src/context.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createContext,
3 | createElement,
4 | useContext,
5 | useMemo,
6 | } from 'react';
7 | import { Store } from 'redux';
8 | import {
9 | PatchedStore,
10 | patchStore,
11 | useSelector as useSelectorOrig,
12 | useTrackedState as useTrackedStateOrig,
13 | } from 'reactive-react-redux';
14 |
15 | import { State, Action } from './state';
16 |
17 | // Context based APIs
18 |
19 | const Context = createContext(new Proxy({}, {
20 | get() { throw new Error('use Provider'); },
21 | }) as PatchedStore);
22 |
23 | export const Provider: React.FC<{ store: Store }> = ({
24 | store,
25 | children,
26 | }) => {
27 | const value = useMemo(() => patchStore(store), [store]);
28 | return createElement(Context.Provider, { value }, children);
29 | };
30 |
31 | export const useDispatch = () => useContext(Context).dispatch;
32 |
33 | export const useSelector = (
34 | selector: (state: State) => Selected,
35 | ) => useSelectorOrig(useContext(Context), selector);
36 |
37 | export const useTrackedState = () => useTrackedStateOrig(useContext(Context));
38 |
--------------------------------------------------------------------------------
/examples/07_multistore/src/index.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line spaced-comment
2 | ///
3 |
4 | import React from 'react';
5 | import { unstable_createRoot as createRoot } from 'react-dom';
6 |
7 | import App from './App';
8 |
9 | const ele = document.getElementById('app');
10 | if (!ele) throw new Error('no app');
11 | createRoot(ele).render(React.createElement(App));
12 |
--------------------------------------------------------------------------------
/examples/07_multistore/src/state.ts:
--------------------------------------------------------------------------------
1 | const initialState = {
2 | count: 0,
3 | person: {
4 | age: 0,
5 | firstName: '',
6 | lastName: '',
7 | },
8 | };
9 |
10 | export type State = typeof initialState;
11 |
12 | export type Action =
13 | | { type: 'increment' }
14 | | { type: 'decrement' }
15 | | { type: 'setFirstName'; firstName: string }
16 | | { type: 'setLastName'; lastName: string }
17 | | { type: 'setAge'; age: number };
18 |
19 | export const reducer = (state = initialState, action: Action) => {
20 | switch (action.type) {
21 | case 'increment': return {
22 | ...state,
23 | count: state.count + 1,
24 | };
25 | case 'decrement': return {
26 | ...state,
27 | count: state.count - 1,
28 | };
29 | case 'setFirstName': return {
30 | ...state,
31 | person: {
32 | ...state.person,
33 | firstName: action.firstName,
34 | },
35 | };
36 | case 'setLastName': return {
37 | ...state,
38 | person: {
39 | ...state.person,
40 | lastName: action.lastName,
41 | },
42 | };
43 | case 'setAge': return {
44 | ...state,
45 | person: {
46 | ...state.person,
47 | age: action.age,
48 | },
49 | };
50 | default: return state;
51 | }
52 | };
53 |
--------------------------------------------------------------------------------
/examples/08_dynamic/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reactive-react-redux-example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@types/react": "latest",
7 | "@types/react-dom": "latest",
8 | "react": "experimental",
9 | "react-dom": "experimental",
10 | "reactive-react-redux": "latest",
11 | "react-scripts": "latest",
12 | "redux": "latest",
13 | "typescript": "latest"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test",
19 | "eject": "react-scripts eject"
20 | },
21 | "browserslist": [
22 | ">0.2%",
23 | "not dead",
24 | "not ie <= 11",
25 | "not op_mini all"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/examples/08_dynamic/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | reactive-react-redux example
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/examples/08_dynamic/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { StrictMode } from 'react';
2 | import { createStore } from 'redux';
3 |
4 | import { reducer } from './state';
5 | import { Provider } from './context';
6 |
7 | import Counter from './Counter';
8 | import Person from './Person';
9 |
10 | const store = createStore(reducer);
11 |
12 | const App = () => (
13 |
14 |
15 | Counter
16 |
17 |
18 | Person
19 |
20 |
21 |
22 |
23 | );
24 |
25 | export default App;
26 |
--------------------------------------------------------------------------------
/examples/08_dynamic/src/Counter.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { useDispatch, useTrackedState } from './context';
4 |
5 | const Counter = () => {
6 | const [index, setIndex] = useState(0);
7 | const state = useTrackedState();
8 | const dispatch = useDispatch();
9 | return (
10 |
11 | {Math.random()}
12 |
13 | Count: {state.count[index]}
14 |
15 |
16 | setIndex(Number(e.target.value) || 0)} />
17 |
18 |
19 | );
20 | };
21 |
22 | export default Counter;
23 |
--------------------------------------------------------------------------------
/examples/08_dynamic/src/Person.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { useDispatch, useTrackedState } from './context';
4 |
5 | const Person = () => {
6 | const [mode, setMode] = useState('first');
7 | const state = useTrackedState();
8 | const dispatch = useDispatch();
9 | return (
10 |
49 | );
50 | };
51 |
52 | export default Person;
53 |
--------------------------------------------------------------------------------
/examples/08_dynamic/src/context.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createContext,
3 | createElement,
4 | useContext,
5 | useMemo,
6 | } from 'react';
7 | import { Store } from 'redux';
8 | import {
9 | PatchedStore,
10 | patchStore,
11 | useSelector as useSelectorOrig,
12 | useTrackedState as useTrackedStateOrig,
13 | } from 'reactive-react-redux';
14 |
15 | import { State, Action } from './state';
16 |
17 | // Context based APIs
18 |
19 | const Context = createContext(new Proxy({}, {
20 | get() { throw new Error('use Provider'); },
21 | }) as PatchedStore);
22 |
23 | export const Provider: React.FC<{ store: Store }> = ({
24 | store,
25 | children,
26 | }) => {
27 | const value = useMemo(() => patchStore(store), [store]);
28 | return createElement(Context.Provider, { value }, children);
29 | };
30 |
31 | export const useDispatch = () => useContext(Context).dispatch;
32 |
33 | export const useSelector = (
34 | selector: (state: State) => Selected,
35 | ) => useSelectorOrig(useContext(Context), selector);
36 |
37 | export const useTrackedState = () => useTrackedStateOrig(useContext(Context));
38 |
--------------------------------------------------------------------------------
/examples/08_dynamic/src/index.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line spaced-comment
2 | ///
3 |
4 | import React from 'react';
5 | import { unstable_createRoot as createRoot } from 'react-dom';
6 |
7 | import App from './App';
8 |
9 | const ele = document.getElementById('app');
10 | if (!ele) throw new Error('no app');
11 | createRoot(ele).render(React.createElement(App));
12 |
--------------------------------------------------------------------------------
/examples/08_dynamic/src/state.ts:
--------------------------------------------------------------------------------
1 | const initialState = {
2 | count: [0, 0, 0],
3 | person: {
4 | age: 0,
5 | firstName: '',
6 | lastName: '',
7 | },
8 | };
9 |
10 | export type State = typeof initialState;
11 |
12 | export type Action =
13 | | { type: 'dummy' } // XXX typescript somehow complaints without this
14 | | { type: 'increment'; index: number }
15 | | { type: 'decrement'; index: number }
16 | | { type: 'setFirstName'; firstName: string }
17 | | { type: 'setLastName'; lastName: string }
18 | | { type: 'setAge'; age: number };
19 |
20 | export const reducer = (state = initialState, action: Action) => {
21 | switch (action.type) {
22 | case 'increment': return {
23 | ...state,
24 | count: [
25 | ...state.count.slice(0, action.index),
26 | state.count[action.index] + 1,
27 | ...state.count.slice(action.index + 1),
28 | ],
29 | };
30 | case 'decrement': return {
31 | ...state,
32 | count: [
33 | ...state.count.slice(0, action.index),
34 | state.count[action.index] - 1,
35 | ...state.count.slice(action.index + 1),
36 | ],
37 | };
38 | case 'setFirstName': return {
39 | ...state,
40 | person: {
41 | ...state.person,
42 | firstName: action.firstName,
43 | },
44 | };
45 | case 'setLastName': return {
46 | ...state,
47 | person: {
48 | ...state.person,
49 | lastName: action.lastName,
50 | },
51 | };
52 | case 'setAge': return {
53 | ...state,
54 | person: {
55 | ...state.person,
56 | age: action.age,
57 | },
58 | };
59 | default: return state;
60 | }
61 | };
62 |
--------------------------------------------------------------------------------
/examples/09_thunk/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reactive-react-redux-example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@types/react": "latest",
7 | "@types/react-dom": "latest",
8 | "react": "experimental",
9 | "react-dom": "experimental",
10 | "reactive-react-redux": "latest",
11 | "react-scripts": "latest",
12 | "redux": "latest",
13 | "redux-thunk": "latest",
14 | "typescript": "latest"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "test": "react-scripts test",
20 | "eject": "react-scripts eject"
21 | },
22 | "browserslist": [
23 | ">0.2%",
24 | "not dead",
25 | "not ie <= 11",
26 | "not op_mini all"
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/examples/09_thunk/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | reactive-react-redux example
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/examples/09_thunk/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { StrictMode } from 'react';
2 | import { createStore, applyMiddleware } from 'redux';
3 | import reduxThunk from 'redux-thunk';
4 |
5 | import { reducer } from './state';
6 | import { Provider } from './context';
7 |
8 | import Counter from './Counter';
9 | import Person from './Person';
10 |
11 | const store = createStore(reducer, applyMiddleware(reduxThunk));
12 |
13 | const App = () => (
14 |
15 |
16 | Counter
17 |
18 |
19 | Person
20 |
21 |
22 |
23 |
24 | );
25 |
26 | export default App;
27 |
--------------------------------------------------------------------------------
/examples/09_thunk/src/Counter.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { useDispatch, useTrackedState } from './context';
4 |
5 | const Counter = () => {
6 | const state = useTrackedState();
7 | const dispatch = useDispatch();
8 | return (
9 |
10 | {Math.random()}
11 |
12 | Count: {state.count}
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default Counter;
21 |
--------------------------------------------------------------------------------
/examples/09_thunk/src/Person.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Dispatch } from 'redux';
3 | import { ThunkDispatch } from 'redux-thunk';
4 |
5 | import { State, Action } from './state';
6 | import { useDispatch, useTrackedState } from './context';
7 |
8 | const Counter: React.FC<{ firstName: string }> = ({ firstName }) => {
9 | const state = useTrackedState();
10 | const dispatch = useDispatch();
11 | return (
12 |
13 | {Math.random()}
14 | {firstName}
15 |
16 | Count: {state.count}
17 |
18 |
19 |
20 |
21 | );
22 | };
23 |
24 | const Person = () => {
25 | const state = useTrackedState();
26 | const dispatch = useDispatch();
27 | const setRandomFirstName = () => {
28 | const dispatchForThunk = dispatch as ThunkDispatch;
29 | dispatchForThunk(async (d: Dispatch) => {
30 | d({ firstName: 'Loading...', type: 'setFirstName' });
31 | try {
32 | const id = Math.floor(100 * Math.random());
33 | const url = `https://jsonplaceholder.typicode.com/posts/${id}`;
34 | const response = await fetch(url);
35 | const body = await response.json();
36 | d({ firstName: body.title.split(' ')[0], type: 'setFirstName' });
37 | } catch (e) {
38 | d({ firstName: 'ERROR: fetching', type: 'setFirstName' });
39 | }
40 | });
41 | };
42 | return (
43 |
44 | {Math.random()}
45 |
46 |
47 |
48 | First Name:
49 | {
52 | const firstName = event.target.value;
53 | dispatch({ firstName, type: 'setFirstName' });
54 | }}
55 | />
56 |
57 |
58 | Last Name:
59 | {
62 | const lastName = event.target.value;
63 | dispatch({ lastName, type: 'setLastName' });
64 | }}
65 | />
66 |
67 |
68 | Age:
69 | {
72 | const age = Number(event.target.value) || 0;
73 | dispatch({ age, type: 'setAge' });
74 | }}
75 | />
76 |
77 |
78 | );
79 | };
80 |
81 | export default Person;
82 |
--------------------------------------------------------------------------------
/examples/09_thunk/src/context.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createContext,
3 | createElement,
4 | useContext,
5 | useMemo,
6 | } from 'react';
7 | import { Store } from 'redux';
8 | import {
9 | PatchedStore,
10 | patchStore,
11 | useSelector as useSelectorOrig,
12 | useTrackedState as useTrackedStateOrig,
13 | } from 'reactive-react-redux';
14 |
15 | import { State, Action } from './state';
16 |
17 | // Context based APIs
18 |
19 | const Context = createContext(new Proxy({}, {
20 | get() { throw new Error('use Provider'); },
21 | }) as PatchedStore);
22 |
23 | export const Provider: React.FC<{ store: Store }> = ({
24 | store,
25 | children,
26 | }) => {
27 | const value = useMemo(() => patchStore(store), [store]);
28 | return createElement(Context.Provider, { value }, children);
29 | };
30 |
31 | export const useDispatch = () => useContext(Context).dispatch;
32 |
33 | export const useSelector = (
34 | selector: (state: State) => Selected,
35 | ) => useSelectorOrig(useContext(Context), selector);
36 |
37 | export const useTrackedState = () => useTrackedStateOrig(useContext(Context));
38 |
--------------------------------------------------------------------------------
/examples/09_thunk/src/index.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line spaced-comment
2 | ///
3 |
4 | import React from 'react';
5 | import { unstable_createRoot as createRoot } from 'react-dom';
6 |
7 | import App from './App';
8 |
9 | const ele = document.getElementById('app');
10 | if (!ele) throw new Error('no app');
11 | createRoot(ele).render(React.createElement(App));
12 |
--------------------------------------------------------------------------------
/examples/09_thunk/src/state.ts:
--------------------------------------------------------------------------------
1 | const initialState = {
2 | count: 0,
3 | person: {
4 | age: 0,
5 | firstName: '',
6 | lastName: '',
7 | },
8 | };
9 |
10 | export type State = typeof initialState;
11 |
12 | export type Action =
13 | | { type: 'increment' }
14 | | { type: 'decrement' }
15 | | { type: 'setFirstName'; firstName: string }
16 | | { type: 'setLastName'; lastName: string }
17 | | { type: 'setAge'; age: number };
18 |
19 | export const reducer = (state = initialState, action: Action) => {
20 | switch (action.type) {
21 | case 'increment': return {
22 | ...state,
23 | count: state.count + 1,
24 | };
25 | case 'decrement': return {
26 | ...state,
27 | count: state.count - 1,
28 | };
29 | case 'setFirstName': return {
30 | ...state,
31 | person: {
32 | ...state.person,
33 | firstName: action.firstName,
34 | },
35 | };
36 | case 'setLastName': return {
37 | ...state,
38 | person: {
39 | ...state.person,
40 | lastName: action.lastName,
41 | },
42 | };
43 | case 'setAge': return {
44 | ...state,
45 | person: {
46 | ...state.person,
47 | age: action.age,
48 | },
49 | };
50 | default: return state;
51 | }
52 | };
53 |
--------------------------------------------------------------------------------
/examples/11_todolist/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reactive-react-redux-example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@types/react": "latest",
7 | "@types/react-dom": "latest",
8 | "react": "experimental",
9 | "react-dom": "experimental",
10 | "reactive-react-redux": "latest",
11 | "react-scripts": "latest",
12 | "redux": "latest",
13 | "typescript": "latest"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test",
19 | "eject": "react-scripts eject"
20 | },
21 | "browserslist": [
22 | ">0.2%",
23 | "not dead",
24 | "not ie <= 11",
25 | "not op_mini all"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/examples/11_todolist/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | reactive-react-redux example
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/examples/11_todolist/src/actions/index.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react';
2 |
3 | import { useDispatch } from '../context';
4 | import { VisibilityFilterType } from '../types';
5 |
6 | let nextTodoId = 0;
7 |
8 | export const useAddTodo = () => {
9 | const dispatch = useDispatch();
10 | return useCallback((text: string) => {
11 | dispatch({
12 | type: 'ADD_TODO',
13 | id: nextTodoId++,
14 | text,
15 | });
16 | }, [dispatch]);
17 | };
18 |
19 | export const useSetVisibilityFilter = () => {
20 | const dispatch = useDispatch();
21 | return useCallback((filter: VisibilityFilterType) => {
22 | dispatch({
23 | type: 'SET_VISIBILITY_FILTER',
24 | filter,
25 | });
26 | }, [dispatch]);
27 | };
28 |
29 | export const useToggleTodo = () => {
30 | const dispatch = useDispatch();
31 | return useCallback((id: number) => {
32 | dispatch({
33 | type: 'TOGGLE_TODO',
34 | id,
35 | });
36 | }, [dispatch]);
37 | };
38 |
--------------------------------------------------------------------------------
/examples/11_todolist/src/components/AddTodo.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { useAddTodo } from '../actions';
4 |
5 | const AddTodo = () => {
6 | const [text, setText] = useState('');
7 | const addTodo = useAddTodo();
8 | return (
9 |
10 |
23 |
24 | );
25 | };
26 |
27 | export default AddTodo;
28 |
--------------------------------------------------------------------------------
/examples/11_todolist/src/components/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Footer from './Footer';
4 | import AddTodo from './AddTodo';
5 | import VisibleTodoList from './VisibleTodoList';
6 |
7 | const App: React.FC = () => (
8 |
13 | );
14 |
15 | export default App;
16 |
--------------------------------------------------------------------------------
/examples/11_todolist/src/components/FilterLink.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { useTrackedState } from '../context';
4 | import { useSetVisibilityFilter } from '../actions';
5 | import { VisibilityFilterType } from '../types';
6 |
7 | type Props = {
8 | filter: VisibilityFilterType;
9 | };
10 |
11 | const FilterLink: React.FC = ({ filter, children }) => {
12 | const state = useTrackedState();
13 | const active = filter === state.visibilityFilter;
14 | const setVisibilityFilter = useSetVisibilityFilter();
15 | return (
16 |
26 | );
27 | };
28 |
29 | export default FilterLink;
30 |
--------------------------------------------------------------------------------
/examples/11_todolist/src/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import FilterLink from './FilterLink';
4 |
5 | const Footer: React.FC = () => (
6 |
7 | Show:
8 | All
9 | Active
10 | Completed
11 |
12 | );
13 |
14 | export default Footer;
15 |
--------------------------------------------------------------------------------
/examples/11_todolist/src/components/Todo.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | type Props = {
4 | onClick: (e: React.MouseEvent) => void;
5 | completed: boolean;
6 | text: string;
7 | };
8 |
9 | const Todo: React.FC = ({ onClick, completed, text }) => (
10 |
18 | {text}
19 |
20 | );
21 |
22 | export default Todo;
23 |
--------------------------------------------------------------------------------
/examples/11_todolist/src/components/VisibleTodoList.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/jsx-props-no-spreading */
2 |
3 | import React from 'react';
4 |
5 | import { useTrackedState } from '../context';
6 | import { TodoType, VisibilityFilterType } from '../types';
7 | import { useToggleTodo } from '../actions';
8 | import Todo from './Todo';
9 |
10 | const getVisibleTodos = (todos: TodoType[], filter: VisibilityFilterType) => {
11 | switch (filter) {
12 | case 'SHOW_ALL':
13 | return todos;
14 | case 'SHOW_COMPLETED':
15 | return todos.filter((t) => t.completed);
16 | case 'SHOW_ACTIVE':
17 | return todos.filter((t) => !t.completed);
18 | default:
19 | throw new Error(`Unknown filter: ${filter}`);
20 | }
21 | };
22 |
23 | const VisibleTodoList: React.FC = () => {
24 | const state = useTrackedState();
25 | const visibleTodos = getVisibleTodos(state.todos, state.visibilityFilter);
26 | const toggleTodo = useToggleTodo();
27 | return (
28 |
29 | {visibleTodos.map((todo) => (
30 | toggleTodo(todo.id)} />
31 | ))}
32 |
33 | );
34 | };
35 |
36 | export default VisibleTodoList;
37 |
--------------------------------------------------------------------------------
/examples/11_todolist/src/context.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createContext,
3 | createElement,
4 | useContext,
5 | useMemo,
6 | } from 'react';
7 | import { Store } from 'redux';
8 | import {
9 | PatchedStore,
10 | patchStore,
11 | useSelector as useSelectorOrig,
12 | useTrackedState as useTrackedStateOrig,
13 | } from 'reactive-react-redux';
14 |
15 | import { State, Action } from './types';
16 |
17 | // Context based APIs
18 |
19 | const Context = createContext(new Proxy({}, {
20 | get() { throw new Error('use Provider'); },
21 | }) as PatchedStore);
22 |
23 | export const Provider: React.FC<{ store: Store }> = ({
24 | store,
25 | children,
26 | }) => {
27 | const value = useMemo(() => patchStore(store), [store]);
28 | return createElement(Context.Provider, { value }, children);
29 | };
30 |
31 | export const useDispatch = () => useContext(Context).dispatch;
32 |
33 | export const useSelector = (
34 | selector: (state: State) => Selected,
35 | ) => useSelectorOrig(useContext(Context), selector);
36 |
37 | export const useTrackedState = () => useTrackedStateOrig(useContext(Context));
38 |
--------------------------------------------------------------------------------
/examples/11_todolist/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import { createStore } from 'redux';
4 |
5 | import { Provider } from './context';
6 | import rootReducer from './reducers';
7 | import App from './components/App';
8 |
9 | const store = createStore(rootReducer);
10 |
11 | render(
12 |
13 |
14 | ,
15 | document.getElementById('app'),
16 | );
17 |
--------------------------------------------------------------------------------
/examples/11_todolist/src/reducers/index.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 |
3 | import todos from './todos';
4 | import visibilityFilter from './visibilityFilter';
5 |
6 | export default combineReducers({
7 | todos,
8 | visibilityFilter,
9 | });
10 |
--------------------------------------------------------------------------------
/examples/11_todolist/src/reducers/todos.ts:
--------------------------------------------------------------------------------
1 | import { TodoType, Action } from '../types';
2 |
3 | const todos = (state: TodoType[] = [], action: Action): TodoType[] => {
4 | switch (action.type) {
5 | case 'ADD_TODO':
6 | return [
7 | ...state,
8 | {
9 | id: action.id,
10 | text: action.text,
11 | completed: false,
12 | },
13 | ];
14 | case 'TOGGLE_TODO':
15 | return state.map((todo: TodoType) => (
16 | todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
17 | ));
18 | default:
19 | return state;
20 | }
21 | };
22 |
23 | export default todos;
24 |
--------------------------------------------------------------------------------
/examples/11_todolist/src/reducers/visibilityFilter.ts:
--------------------------------------------------------------------------------
1 | import { Action, VisibilityFilterType } from '../types';
2 |
3 | const visibilityFilter = (
4 | state: VisibilityFilterType = 'SHOW_ALL',
5 | action: Action,
6 | ): VisibilityFilterType => {
7 | switch (action.type) {
8 | case 'SET_VISIBILITY_FILTER':
9 | return action.filter;
10 | default:
11 | return state;
12 | }
13 | };
14 |
15 | export default visibilityFilter;
16 |
--------------------------------------------------------------------------------
/examples/11_todolist/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export type VisibilityFilterType =
2 | | 'SHOW_ALL'
3 | | 'SHOW_COMPLETED'
4 | | 'SHOW_ACTIVE';
5 |
6 | export type TodoType = {
7 | id: number;
8 | text: string;
9 | completed: boolean;
10 | };
11 |
12 | export type State = {
13 | todos: TodoType[];
14 | visibilityFilter: VisibilityFilterType;
15 | };
16 |
17 | export type Action =
18 | | { type: 'ADD_TODO'; id: number; text: string }
19 | | { type: 'SET_VISIBILITY_FILTER'; filter: VisibilityFilterType }
20 | | { type: 'TOGGLE_TODO'; id: number };
21 |
--------------------------------------------------------------------------------
/examples/12_async/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reactive-react-redux-example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@types/react": "latest",
7 | "@types/react-dom": "latest",
8 | "react": "experimental",
9 | "react-dom": "experimental",
10 | "reactive-react-redux": "latest",
11 | "react-scripts": "latest",
12 | "redux": "latest",
13 | "redux-thunk": "latest",
14 | "typescript": "latest"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "test": "react-scripts test",
20 | "eject": "react-scripts eject"
21 | },
22 | "browserslist": [
23 | ">0.2%",
24 | "not dead",
25 | "not ie <= 11",
26 | "not op_mini all"
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/examples/12_async/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | reactive-react-redux example
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/examples/12_async/src/components/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect } from 'react';
2 |
3 | import { useSelector } from '../context';
4 | import { State, SelectedSubreddit } from '../store/actions';
5 | import useSelectSubreddit from '../hooks/useSelectSubreddit';
6 | import useFetchPostsIfNeeded from '../hooks/useFetchPostsIfNeeded';
7 | import useInvalidateSubreddit from '../hooks/useInvalidateSubreddit';
8 |
9 | import Picker from './Picker';
10 | import Posts from './Posts';
11 |
12 | const App: React.FC = () => {
13 | const selectedSubreddit = useSelector((state: State) => state.selectedSubreddit);
14 | const postsBySubreddit = useSelector((state: State) => state.postsBySubreddit);
15 | const {
16 | isFetching,
17 | items: posts,
18 | lastUpdated,
19 | } = postsBySubreddit[selectedSubreddit] || {
20 | isFetching: true,
21 | items: [],
22 | lastUpdated: undefined,
23 | };
24 |
25 | const fetchPostsIfNeeded = useFetchPostsIfNeeded();
26 | useEffect(() => {
27 | fetchPostsIfNeeded(selectedSubreddit);
28 | }, [fetchPostsIfNeeded, selectedSubreddit]);
29 |
30 | const selectSubreddit = useSelectSubreddit();
31 | const handleChange = useCallback((nextSubreddit: SelectedSubreddit) => {
32 | selectSubreddit(nextSubreddit);
33 | }, [selectSubreddit]);
34 |
35 | const invalidateSubreddit = useInvalidateSubreddit();
36 | const handleRefreshClick = (e: React.MouseEvent) => {
37 | e.preventDefault();
38 | invalidateSubreddit(selectedSubreddit);
39 | fetchPostsIfNeeded(selectedSubreddit);
40 | };
41 |
42 | const isEmpty = posts.length === 0;
43 | return (
44 |
45 |
50 |
51 | {lastUpdated && (
52 |
53 | Last updated at {new Date(lastUpdated).toLocaleTimeString()}.
54 |
55 | )}
56 | {!isFetching && (
57 |
60 | )}
61 |
62 | {isEmpty && isFetching &&
Loading...
}
63 | {isEmpty && !isFetching &&
Empty.
}
64 | {!isEmpty && (
65 |
68 | )}
69 |
70 | );
71 | };
72 |
73 | export default App;
74 |
--------------------------------------------------------------------------------
/examples/12_async/src/components/Picker.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Picker: React.FC<{
4 | value: string;
5 | onChange: (value: string) => void;
6 | options: string[];
7 | }> = ({ value, onChange, options }) => (
8 |
9 | {value}
10 |
20 |
21 | );
22 |
23 | export default Picker;
24 |
--------------------------------------------------------------------------------
/examples/12_async/src/components/Posts.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Posts: React.FC<{
4 | posts: {
5 | id: string;
6 | title: string;
7 | }[];
8 | }> = ({ posts }) => (
9 |
10 | {posts.map((post) => (
11 | - {post.title}
12 | ))}
13 |
14 | );
15 |
16 | export default Posts;
17 |
--------------------------------------------------------------------------------
/examples/12_async/src/context.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createContext,
3 | createElement,
4 | useContext,
5 | useMemo,
6 | } from 'react';
7 | import { Store } from 'redux';
8 | import {
9 | PatchedStore,
10 | patchStore,
11 | useSelector as useSelectorOrig,
12 | useTrackedState as useTrackedStateOrig,
13 | } from 'reactive-react-redux';
14 |
15 | import { State, Action } from './store/actions';
16 |
17 | // Context based APIs
18 |
19 | const Context = createContext(new Proxy({}, {
20 | get() { throw new Error('use Provider'); },
21 | }) as PatchedStore);
22 |
23 | export const Provider: React.FC<{ store: Store }> = ({
24 | store,
25 | children,
26 | }) => {
27 | const value = useMemo(() => patchStore(store), [store]);
28 | return createElement(Context.Provider, { value }, children);
29 | };
30 |
31 | export const useDispatch = () => useContext(Context).dispatch;
32 |
33 | export const useSelector = (
34 | selector: (state: State) => Selected,
35 | ) => useSelectorOrig(useContext(Context), selector);
36 |
37 | export const useTrackedState = () => useTrackedStateOrig(useContext(Context));
38 |
39 | export const useStore = () => useContext(Context);
40 |
--------------------------------------------------------------------------------
/examples/12_async/src/hooks/useFetchPostsIfNeeded.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react';
2 |
3 | import { useDispatch, useStore } from '../context';
4 | import { State, Post } from '../store/actions';
5 |
6 | const shouldFetchPosts = (state: State, subreddit: string) => {
7 | const posts = state.postsBySubreddit[subreddit];
8 | if (!posts) {
9 | return true;
10 | }
11 | if (posts.isFetching) {
12 | return false;
13 | }
14 | return posts.didInvalidate;
15 | };
16 |
17 | const extractPosts = (json: unknown): Post[] | null => {
18 | try {
19 | const posts: Post[] = (json as {
20 | data: {
21 | children: {
22 | data: {
23 | id: string;
24 | title: string;
25 | };
26 | }[];
27 | };
28 | }).data.children.map((child) => child.data);
29 | // type check
30 | if (posts.every((post) => (
31 | typeof post.id === 'string' && typeof post.title === 'string'
32 | ))) {
33 | return posts;
34 | }
35 | return null;
36 | } catch (e) {
37 | return null;
38 | }
39 | };
40 |
41 | const useFetchPostsIfNeeded = () => {
42 | const dispatch = useDispatch();
43 | const store = useStore();
44 | const fetchPostsIfNeeded = useCallback(async (subreddit: string) => {
45 | if (!shouldFetchPosts(store.getState(), subreddit)) {
46 | return;
47 | }
48 | dispatch({
49 | type: 'REQUEST_POSTS',
50 | subreddit,
51 | });
52 | const response = await fetch(`https://www.reddit.com/r/${subreddit}.json`);
53 | const json = await response.json();
54 | const posts = extractPosts(json);
55 | if (!posts) throw new Error('unexpected json format');
56 | dispatch({
57 | type: 'RECEIVE_POSTS',
58 | subreddit,
59 | posts,
60 | receivedAt: Date.now(),
61 | });
62 | }, [dispatch, store]);
63 | return fetchPostsIfNeeded;
64 | };
65 |
66 | export default useFetchPostsIfNeeded;
67 |
--------------------------------------------------------------------------------
/examples/12_async/src/hooks/useInvalidateSubreddit.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react';
2 |
3 | import { useDispatch } from '../context';
4 |
5 | const useInvalidateSubreddit = () => {
6 | const dispatch = useDispatch();
7 | const invalidateSubreddit = useCallback((subreddit: string) => {
8 | dispatch({
9 | type: 'INVALIDATE_SUBREDDIT',
10 | subreddit,
11 | });
12 | }, [dispatch]);
13 | return invalidateSubreddit;
14 | };
15 |
16 | export default useInvalidateSubreddit;
17 |
--------------------------------------------------------------------------------
/examples/12_async/src/hooks/useSelectSubreddit.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react';
2 |
3 | import { useDispatch } from '../context';
4 |
5 | const useSelectSubreddit = () => {
6 | const dispatch = useDispatch();
7 | const selectSubreddit = useCallback((subreddit: string) => {
8 | dispatch({
9 | type: 'SELECT_SUBREDDIT',
10 | subreddit,
11 | });
12 | }, [dispatch]);
13 | return selectSubreddit;
14 | };
15 |
16 | export default useSelectSubreddit;
17 |
--------------------------------------------------------------------------------
/examples/12_async/src/index.tsx:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line spaced-comment
2 | ///
3 |
4 | import React from 'react';
5 | import { unstable_createRoot as createRoot } from 'react-dom';
6 | import { createStore } from 'redux';
7 |
8 | import { Provider } from './context';
9 | import rootReducer from './store/reducers';
10 | import App from './components/App';
11 |
12 | const store = createStore(rootReducer);
13 |
14 | const ele = document.getElementById('app');
15 | if (!ele) throw new Error('no app');
16 | createRoot(ele).render(
17 |
18 |
19 | ,
20 | );
21 |
--------------------------------------------------------------------------------
/examples/12_async/src/store/actions.ts:
--------------------------------------------------------------------------------
1 | export type Post = {
2 | id: string;
3 | title: string;
4 | };
5 |
6 | export type SubredditPosts = {
7 | isFetching: boolean;
8 | didInvalidate: boolean;
9 | items: Post[];
10 | lastUpdated?: number;
11 | };
12 |
13 | export type PostsBySubreddit = {
14 | [subreddit: string]: SubredditPosts;
15 | };
16 |
17 | export type SelectedSubreddit = string;
18 |
19 | export type State = {
20 | selectedSubreddit: SelectedSubreddit;
21 | postsBySubreddit: PostsBySubreddit;
22 | };
23 |
24 | type SelectSubredditAction = {
25 | type: 'SELECT_SUBREDDIT';
26 | subreddit: string;
27 | };
28 |
29 | type InvalidateSubredditAction = {
30 | type: 'INVALIDATE_SUBREDDIT';
31 | subreddit: string;
32 | };
33 |
34 | type RequestPostsAction = {
35 | type: 'REQUEST_POSTS';
36 | subreddit: string;
37 | };
38 |
39 | type ReceivePostsAction = {
40 | type: 'RECEIVE_POSTS';
41 | subreddit: string;
42 | posts: Post[];
43 | receivedAt: number;
44 | };
45 |
46 | export type Action =
47 | | SelectSubredditAction
48 | | InvalidateSubredditAction
49 | | RequestPostsAction
50 | | ReceivePostsAction;
51 |
--------------------------------------------------------------------------------
/examples/12_async/src/store/reducers.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import {
3 | SubredditPosts,
4 | SelectedSubreddit,
5 | PostsBySubreddit,
6 | State,
7 | Action,
8 | } from './actions';
9 |
10 | const selectedSubreddit = (
11 | state: SelectedSubreddit = 'reactjs',
12 | action: Action,
13 | ): SelectedSubreddit => {
14 | switch (action.type) {
15 | case 'SELECT_SUBREDDIT':
16 | return action.subreddit;
17 | default:
18 | return state;
19 | }
20 | };
21 |
22 | const posts = (state: SubredditPosts = {
23 | isFetching: false,
24 | didInvalidate: false,
25 | items: [],
26 | }, action: Action): SubredditPosts => {
27 | switch (action.type) {
28 | case 'INVALIDATE_SUBREDDIT':
29 | return {
30 | ...state,
31 | didInvalidate: true,
32 | };
33 | case 'REQUEST_POSTS':
34 | return {
35 | ...state,
36 | isFetching: true,
37 | didInvalidate: false,
38 | };
39 | case 'RECEIVE_POSTS':
40 | return {
41 | ...state,
42 | isFetching: false,
43 | didInvalidate: false,
44 | items: action.posts,
45 | lastUpdated: action.receivedAt,
46 | };
47 | default:
48 | return state;
49 | }
50 | };
51 |
52 | const postsBySubreddit = (
53 | state: PostsBySubreddit = {},
54 | action: Action,
55 | ): PostsBySubreddit => {
56 | switch (action.type) {
57 | case 'INVALIDATE_SUBREDDIT':
58 | case 'RECEIVE_POSTS':
59 | case 'REQUEST_POSTS':
60 | return {
61 | ...state,
62 | [action.subreddit]: posts(state[action.subreddit], action),
63 | };
64 | default:
65 | return state;
66 | }
67 | };
68 |
69 | const rootReducer = combineReducers({
70 | postsBySubreddit,
71 | selectedSubreddit,
72 | });
73 |
74 | export default rootReducer;
75 |
--------------------------------------------------------------------------------
/examples/13_memo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reactive-react-redux-example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@types/react": "latest",
7 | "@types/react-dom": "latest",
8 | "react": "experimental",
9 | "react-dom": "experimental",
10 | "reactive-react-redux": "latest",
11 | "react-scripts": "latest",
12 | "redux": "latest",
13 | "typescript": "latest"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test",
19 | "eject": "react-scripts eject"
20 | },
21 | "browserslist": [
22 | ">0.2%",
23 | "not dead",
24 | "not ie <= 11",
25 | "not op_mini all"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/examples/13_memo/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | reactive-react-redux example
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/examples/13_memo/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { StrictMode } from 'react';
2 | import { createStore } from 'redux';
3 |
4 | import { reducer } from './state';
5 | import { Provider } from './context';
6 |
7 | import TodoList from './TodoList';
8 |
9 | const store = createStore(reducer);
10 |
11 | const App = () => (
12 |
13 |
14 |
15 |
16 |
17 | );
18 |
19 | export default App;
20 |
--------------------------------------------------------------------------------
/examples/13_memo/src/TodoItem.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { memo } from 'reactive-react-redux';
4 | import { useDispatch } from './context';
5 | import { TodoType } from './state';
6 |
7 | type Props = {
8 | todo: TodoType;
9 | };
10 |
11 | let numRendered = 0;
12 |
13 | const TodoItem: React.FC = ({ todo }) => {
14 | const dispatch = useDispatch();
15 | return (
16 |
17 | numRendered: {++numRendered}
18 | dispatch({ type: 'TOGGLE_TODO', id: todo.id })}
22 | />
23 |
28 | {todo.title}
29 |
30 |
31 | );
32 | };
33 |
34 | // export default React.memo(TodoItem); // Instead of React.memo
35 | export default memo(TodoItem); // Use custom memo
36 |
--------------------------------------------------------------------------------
/examples/13_memo/src/TodoList.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { useTrackedState } from './context';
4 | import TodoItem from './TodoItem';
5 |
6 | const TodoList: React.FC = () => {
7 | const state = useTrackedState();
8 | const { todos } = state;
9 | return (
10 |
11 | {todos.map((todo) => (
12 |
13 | ))}
14 |
15 | );
16 | };
17 |
18 | export default TodoList;
19 |
--------------------------------------------------------------------------------
/examples/13_memo/src/context.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createContext,
3 | createElement,
4 | useContext,
5 | useMemo,
6 | } from 'react';
7 | import { Store } from 'redux';
8 | import {
9 | PatchedStore,
10 | patchStore,
11 | useSelector as useSelectorOrig,
12 | useTrackedState as useTrackedStateOrig,
13 | } from 'reactive-react-redux';
14 |
15 | import { State, Action } from './state';
16 |
17 | // Context based APIs
18 |
19 | const Context = createContext(new Proxy({}, {
20 | get() { throw new Error('use Provider'); },
21 | }) as PatchedStore);
22 |
23 | export const Provider: React.FC<{ store: Store }> = ({
24 | store,
25 | children,
26 | }) => {
27 | const value = useMemo(() => patchStore(store), [store]);
28 | return createElement(Context.Provider, { value }, children);
29 | };
30 |
31 | export const useDispatch = () => useContext(Context).dispatch;
32 |
33 | export const useSelector = (
34 | selector: (state: State) => Selected,
35 | ) => useSelectorOrig(useContext(Context), selector);
36 |
37 | export const useTrackedState = () => useTrackedStateOrig(useContext(Context));
38 |
--------------------------------------------------------------------------------
/examples/13_memo/src/index.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line spaced-comment
2 | ///
3 |
4 | import React from 'react';
5 | import { unstable_createRoot as createRoot } from 'react-dom';
6 |
7 | import App from './App';
8 |
9 | const ele = document.getElementById('app');
10 | if (!ele) throw new Error('no app');
11 | createRoot(ele).render(React.createElement(App));
12 |
--------------------------------------------------------------------------------
/examples/13_memo/src/state.ts:
--------------------------------------------------------------------------------
1 | export type TodoType = {
2 | id: number;
3 | title: string;
4 | completed: boolean;
5 | };
6 |
7 | export type State = {
8 | todos: TodoType[];
9 | };
10 |
11 | export type Action =
12 | | { type: 'ADD_TODO'; title: string }
13 | | { type: 'DELETE_TODO'; id: number }
14 | | { type: 'CHANGE_TODO'; id: number; title: string }
15 | | { type: 'TOGGLE_TODO'; id: number };
16 |
17 | const initialState: State = {
18 | todos: [
19 | { id: 1, title: 'Wash dishes', completed: false },
20 | { id: 2, title: 'Study JS', completed: false },
21 | { id: 3, title: 'Buy ticket', completed: false },
22 | ],
23 | };
24 |
25 | let nextId = 4;
26 |
27 | export const reducer = (state = initialState, action: Action) => {
28 | switch (action.type) {
29 | case 'ADD_TODO':
30 | return {
31 | ...state,
32 | todos: [
33 | ...state.todos,
34 | { id: nextId++, title: action.title, completed: false },
35 | ],
36 | };
37 | case 'DELETE_TODO':
38 | return {
39 | ...state,
40 | todos: state.todos.filter((todo) => todo.id !== action.id),
41 | };
42 | case 'CHANGE_TODO':
43 | return {
44 | ...state,
45 | todos: state.todos.map((todo) => (
46 | todo.id === action.id ? { ...todo, title: action.title } : todo
47 | )),
48 | };
49 | case 'TOGGLE_TODO':
50 | return {
51 | ...state,
52 | todos: state.todos.map((todo) => (
53 | todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
54 | )),
55 | };
56 | default:
57 | return state;
58 | }
59 | };
60 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reactive-react-redux",
3 | "description": "React Redux binding with React Hooks and Proxy",
4 | "version": "5.0.0-alpha.7",
5 | "publishConfig": {
6 | "tag": "next"
7 | },
8 | "author": "Daishi Kato",
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/dai-shi/reactive-react-redux.git"
12 | },
13 | "source": "./src/index.ts",
14 | "main": "./dist/index.umd.js",
15 | "module": "./dist/index.modern.js",
16 | "types": "./dist/src/index.d.ts",
17 | "sideEffects": false,
18 | "files": [
19 | "src",
20 | "dist"
21 | ],
22 | "scripts": {
23 | "compile": "microbundle build -f modern,umd",
24 | "test": "run-s eslint tsc-test jest",
25 | "eslint": "eslint --ext .js,.ts,.tsx --ignore-pattern dist .",
26 | "jest": "jest --preset ts-jest/presets/js-with-ts",
27 | "tsc-test": "tsc --project . --noEmit",
28 | "apidoc": "documentation readme --section API --markdown-toc false --parse-extension ts src/*.ts",
29 | "examples:01_minimal": "DIR=01_minimal EXT=js webpack-dev-server",
30 | "examples:02_typescript": "DIR=02_typescript webpack-dev-server",
31 | "examples:03_deep": "DIR=03_deep webpack-dev-server",
32 | "examples:04_immer": "DIR=04_immer webpack-dev-server",
33 | "examples:05_localstate": "DIR=05_localstate webpack-dev-server",
34 | "examples:06_memoization": "DIR=06_memoization webpack-dev-server",
35 | "examples:07_multistore": "DIR=07_multistore webpack-dev-server",
36 | "examples:08_dynamic": "DIR=08_dynamic webpack-dev-server",
37 | "examples:09_thunk": "DIR=09_thunk webpack-dev-server",
38 | "examples:11_todolist": "DIR=11_todolist EXT=tsx webpack-dev-server",
39 | "examples:12_async": "DIR=12_async EXT=tsx webpack-dev-server",
40 | "examples:13_memo": "DIR=13_memo webpack-dev-server"
41 | },
42 | "keywords": [
43 | "react",
44 | "redux",
45 | "state",
46 | "hooks",
47 | "stateless",
48 | "thisless",
49 | "pure"
50 | ],
51 | "license": "MIT",
52 | "dependencies": {
53 | "proxy-compare": "^1.1.3"
54 | },
55 | "devDependencies": {
56 | "@testing-library/react": "^11.2.2",
57 | "@types/jest": "^26.0.19",
58 | "@types/react": "^17.0.0",
59 | "@types/react-dom": "^17.0.0",
60 | "@types/redux-logger": "^3.0.8",
61 | "@typescript-eslint/eslint-plugin": "^4.11.1",
62 | "@typescript-eslint/parser": "^4.11.1",
63 | "documentation": "^13.1.0",
64 | "eslint": "^7.16.0",
65 | "eslint-config-airbnb": "^18.2.1",
66 | "eslint-plugin-import": "^2.22.1",
67 | "eslint-plugin-jsx-a11y": "^6.4.1",
68 | "eslint-plugin-react": "^7.21.5",
69 | "eslint-plugin-react-hooks": "^4.2.0",
70 | "html-webpack-plugin": "^4.5.0",
71 | "immer": "^8.0.0",
72 | "jest": "^26.6.3",
73 | "microbundle": "^0.13.0",
74 | "npm-run-all": "^4.1.5",
75 | "react": "experimental",
76 | "react-dom": "experimental",
77 | "redux": "^4.0.5",
78 | "redux-thunk": "^2.3.0",
79 | "ts-jest": "^26.4.4",
80 | "ts-loader": "^8.0.12",
81 | "typescript": "^4.1.3",
82 | "webpack": "^4.44.2",
83 | "webpack-cli": "^3.3.12",
84 | "webpack-dev-server": "^3.11.1"
85 | },
86 | "peerDependencies": {
87 | "react": ">=18.0.0",
88 | "redux": ">=4.0.0"
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export type { PatchedStore } from './patchStore';
2 | export { patchStore } from './patchStore';
3 | export { useSelector } from './useSelector';
4 | export { useTrackedState } from './useTrackedState';
5 | export { memo } from './memo';
6 | export { getUntrackedObject } from 'proxy-compare';
7 |
--------------------------------------------------------------------------------
/src/memo.ts:
--------------------------------------------------------------------------------
1 | import { ComponentProps, createElement, memo as reactMemo } from 'react';
2 | import { trackMemo } from 'proxy-compare';
3 |
4 | /**
5 | * memo
6 | *
7 | * Using `React.memo` with tracked state is not compatible,
8 | * because `React.memo` stops state access, thus no tracking occurs.
9 | * This is a special memo to be used instead of `React.memo` with tracking support.
10 | *
11 | * @example
12 | * import { memo } from 'reactive-react-redux';
13 | *
14 | * const ChildComponent = memo(({ obj1, obj2 }) => {
15 | * // ...
16 | * });
17 | */
18 | export const memo = (
19 | Component: Parameters[0],
20 | areEqual?: Parameters[1],
21 | ) => {
22 | const WrappedComponent = (props: ComponentProps) => {
23 | Object.values(props).forEach(trackMemo);
24 | return createElement(Component, props);
25 | };
26 | return reactMemo(WrappedComponent, areEqual);
27 | };
28 |
--------------------------------------------------------------------------------
/src/patchStore.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/ban-ts-comment */
2 |
3 | import {
4 | // @ts-ignore
5 | unstable_createMutableSource as createMutableSource,
6 | } from 'react';
7 | import {
8 | Action as ReduxAction,
9 | Store,
10 | } from 'redux';
11 |
12 | export type PatchedStore> = {
13 | mutableSource: any;
14 | } & Store;
15 |
16 | /**
17 | * patch Redux store for React
18 | *
19 | * @example
20 | * import { createStore } from 'redux';
21 | * import { patchStore } from 'reactive-react-redux';
22 | *
23 | * const reducer = ...;
24 | * const store = patchStore(createStore(reducer));
25 | */
26 | export const patchStore = >(
27 | store: Store,
28 | ) => {
29 | const mutableSource = createMutableSource(store, () => store.getState());
30 | (store as PatchedStore).mutableSource = mutableSource;
31 | return store as PatchedStore;
32 | };
33 |
--------------------------------------------------------------------------------
/src/useSelector.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/ban-ts-comment */
2 |
3 | import {
4 | useCallback,
5 | // @ts-ignore
6 | unstable_useMutableSource as useMutableSource,
7 | } from 'react';
8 | import {
9 | Action as ReduxAction,
10 | Store,
11 | } from 'redux';
12 |
13 | import { PatchedStore } from './patchStore';
14 |
15 | const subscribe = >(
16 | store: Store,
17 | callback: () => void,
18 | ) => store.subscribe(callback);
19 |
20 | /**
21 | * useSelector hook
22 | *
23 | * selector has to be stable. Either define it outside render
24 | * or use useCallback if selector uses props.
25 | *
26 | * @example
27 | * import { useCallback } from 'react';
28 | * import { useSelector } from 'reactive-react-redux';
29 | *
30 | * const Component = ({ count }) => {
31 | * const isBigger = useSelector(store, useCallack(state => state.count > count, [count]));
32 | * ...
33 | * };
34 | */
35 | export const useSelector = , Selected>(
36 | patchedStore: PatchedStore,
37 | selector: (state: State) => Selected,
38 | ) => {
39 | const { mutableSource } = patchedStore;
40 | const getSnapshot = useCallback((store: Store) => (
41 | selector(store.getState())
42 | ), [selector]);
43 | const selected: Selected = useMutableSource(mutableSource, getSnapshot, subscribe);
44 | return selected;
45 | };
46 |
--------------------------------------------------------------------------------
/src/useTrackedState.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/ban-ts-comment */
2 |
3 | import {
4 | useCallback,
5 | useEffect,
6 | useLayoutEffect,
7 | useMemo,
8 | useReducer,
9 | useRef,
10 | // @ts-ignore
11 | unstable_useMutableSource as useMutableSource,
12 | } from 'react';
13 | import { Action as ReduxAction, Store } from 'redux';
14 | import { createDeepProxy, isDeepChanged } from 'proxy-compare';
15 |
16 | import { PatchedStore } from './patchStore';
17 | import { useAffectedDebugValue } from './utils';
18 |
19 | const isSSR = typeof window === 'undefined'
20 | || /ServerSideRendering/.test(window.navigator && window.navigator.userAgent);
21 |
22 | const useIsomorphicLayoutEffect = isSSR ? useEffect : useLayoutEffect;
23 |
24 | const getSnapshot = >(
25 | store: Store,
26 | ) => store.getState();
27 |
28 | /**
29 | * useTrackedState hook
30 | *
31 | * It return the Redux state wrapped by Proxy,
32 | * and the state prperty access is tracked.
33 | * It will only re-render if accessed properties are changed.
34 | *
35 | * @example
36 | * import { useTrackedState } from 'reactive-react-redux';
37 | *
38 | * const Component = () => {
39 | * const state = useTrackedState(store);
40 | * ...
41 | * };
42 | */
43 | export const useTrackedState = >(
44 | patchedStore: PatchedStore,
45 | ) => {
46 | const { mutableSource } = patchedStore;
47 | const [version, forceUpdate] = useReducer((c) => c + 1, 0);
48 | const affected = new WeakMap();
49 | const lastAffected = useRef, unknown>>();
50 | const prevState = useRef();
51 | const lastState = useRef();
52 | useIsomorphicLayoutEffect(() => {
53 | prevState.current = patchedStore.getState();
54 | lastState.current = patchedStore.getState();
55 | }, [patchedStore]);
56 | useIsomorphicLayoutEffect(() => {
57 | lastAffected.current = affected;
58 | if (prevState.current !== lastState.current
59 | && isDeepChanged(
60 | prevState.current,
61 | lastState.current,
62 | affected,
63 | new WeakMap(),
64 | )) {
65 | prevState.current = lastState.current;
66 | forceUpdate();
67 | }
68 | });
69 | const sub = useCallback((store: Store, cb: () => void) => store.subscribe(() => {
70 | const nextState = store.getState();
71 | lastState.current = nextState;
72 | if (prevState.current
73 | && lastAffected.current
74 | && !isDeepChanged(
75 | prevState.current,
76 | nextState,
77 | lastAffected.current,
78 | new WeakMap(),
79 | )
80 | ) {
81 | // not changed
82 | return;
83 | }
84 | prevState.current = nextState;
85 | cb();
86 | }), [version]); // eslint-disable-line react-hooks/exhaustive-deps
87 | const state: State = useMutableSource(mutableSource, getSnapshot, sub);
88 | if (process.env.NODE_ENV !== 'production') {
89 | // eslint-disable-next-line react-hooks/rules-of-hooks
90 | useAffectedDebugValue(state, affected);
91 | }
92 | const proxyCache = useMemo(() => new WeakMap(), []); // per-hook proxyCache
93 | return createDeepProxy(state, affected, proxyCache);
94 | };
95 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import {
2 | useEffect,
3 | useRef,
4 | useDebugValue,
5 | } from 'react';
6 | import { affectedToPathList } from 'proxy-compare';
7 |
8 | type Obj = Record;
9 |
10 | export const useAffectedDebugValue = (
11 | state: State,
12 | affected: WeakMap,
13 | ) => {
14 | const pathList = useRef<(string | number | symbol)[][]>();
15 | useEffect(() => {
16 | pathList.current = affectedToPathList(state, affected);
17 | });
18 | useDebugValue(pathList.current);
19 | };
20 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "target": "es5",
5 | "esModuleInterop": true,
6 | "module": "es2015",
7 | "moduleResolution": "node",
8 | "jsx": "react",
9 | "allowJs": true,
10 | "noUnusedLocals": true,
11 | "noUnusedParameters": true,
12 | "sourceMap": true,
13 | "baseUrl": ".",
14 | "paths": {
15 | "reactive-react-redux": ["./src"]
16 | },
17 | "outDir": "./dist"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-var-requires
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 |
4 | const { DIR, EXT = 'ts' } = process.env;
5 |
6 | module.exports = {
7 | mode: 'development',
8 | devtool: 'cheap-module-eval-source-map',
9 | entry: `./examples/${DIR}/src/index.${EXT}`,
10 | output: {
11 | publicPath: '/',
12 | },
13 | plugins: [
14 | new HtmlWebpackPlugin({
15 | template: `./examples/${DIR}/public/index.html`,
16 | }),
17 | ],
18 | module: {
19 | rules: [{
20 | test: /\.[jt]sx?$/,
21 | exclude: /node_modules/,
22 | loader: 'ts-loader',
23 | options: {
24 | transpileOnly: true,
25 | },
26 | }],
27 | },
28 | resolve: {
29 | extensions: ['.js', '.jsx', '.ts', '.tsx'],
30 | alias: {
31 | 'reactive-react-redux': `${__dirname}/src`,
32 | },
33 | },
34 | devServer: {
35 | port: process.env.PORT || '8080',
36 | contentBase: `./examples/${DIR}/public`,
37 | historyApiFallback: true,
38 | },
39 | };
40 |
--------------------------------------------------------------------------------