├── .eslintrc.json
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── LICENSE
├── README.md
├── __tests__
└── all_spec.js
├── jest-puppeteer.config.js
├── package.json
├── public
└── index.html
├── src
├── apollo-client
│ └── index.js
├── common.js
├── constate
│ └── index.js
├── effector
│ └── index.js
├── index.js
├── jotai
│ └── index.js
├── mobx-react-lite
│ └── index.js
├── react-hooks-global-state
│ └── index.js
├── react-query
│ └── index.js
├── react-redux
│ └── index.js
├── react-rxjs
│ └── index.js
├── react-state
│ └── index.js
├── react-tracked
│ └── index.js
├── recoil
│ └── index.js
├── recoil_UNSTABLE
│ └── index.js
├── simplux
│ └── index.js
├── use-atom
│ └── index.js
├── use-context-selector-base
│ └── index.js
├── use-context-selector
│ └── index.js
├── use-subscription
│ └── index.js
├── valtio
│ └── index.js
└── zustand
│ └── index.js
├── update_readme.js
├── webpack.config.js
└── yarn.lock
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | "react-hooks"
4 | ],
5 | "extends": [
6 | "airbnb"
7 | ],
8 | "env": {
9 | "browser": true
10 | },
11 | "rules": {
12 | "react-hooks/rules-of-hooks": "error",
13 | "react-hooks/exhaustive-deps": "error",
14 | "react/jsx-filename-extension": ["error", { "extensions": [".js"] }],
15 | "react/prop-types": "off",
16 | "no-param-reassign": "off",
17 | "no-console": "off",
18 | "no-await-in-loop": "off",
19 | "react/function-component-definition": ["error", { "namedComponents": "arrow-function" }]
20 | },
21 | "overrides": [{
22 | "files": ["__tests__/**/*"],
23 | "env": {
24 | "jest": true
25 | },
26 | "rules": {
27 | "import/no-extraneous-dependencies": ["error", { "devDependencies": true }]
28 | }
29 | }]
30 | }
31 |
--------------------------------------------------------------------------------
/.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: '14.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 | /.idea
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019-2021 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 | # Will this React global state work in concurrent rendering?
2 |
3 | Test tearing and branching in React concurrent rendering
4 |
5 | - [Discussion in React 18 WG](https://github.com/reactwg/react-18/discussions/116)
6 |
7 | ## Introduction
8 |
9 | React 18 comes with a new feature called "concurrent rendering".
10 | With global state, there's a theoretical issue called "tearing"
11 | that might occur in React concurrent rendering.
12 |
13 | Let's test the behavior!
14 |
15 | ## What is tearing?
16 |
17 | - [What is tearing in React 18 WG](https://github.com/reactwg/react-18/discussions/69)
18 | - [Stack Overflow](https://stackoverflow.com/questions/54891675/what-is-tearing-in-the-context-of-the-react-redux)
19 | - [Talk by Mark Erikson](https://www.youtube.com/watch?v=yOZ4Ml9LlWE&t=933s)
20 | - [Talk by Flarnie Marchan](https://www.youtube.com/watch?v=V1Ly-8Z1wQA&t=1079s)
21 | - Some other resources
22 | - https://github.com/reactjs/rfcs/pull/147
23 | - https://gist.github.com/bvaughn/054b82781bec875345bd85a5b1344698
24 |
25 | ## What is branching?
26 |
27 | - Old resources
28 | - https://reactjs.org/docs/concurrent-mode-intro.html
29 |
30 | ## How does it work?
31 |
32 | A small app is implemented with each library.
33 | The state has one count.
34 | The app shows the count in fifty components.
35 |
36 | There's a button outside of React and
37 | if it's clicked it will trigger state mutation.
38 | This is to emulate mutating an external state outside of React,
39 | for example updating state by Redux middleware.
40 |
41 | The render has intentionally expensive computation.
42 | If the mutation happens during rendering with in a tree,
43 | there could be an inconsistency in the state.
44 | If it finds the inconsistency, the test will fail.
45 |
46 | ## How to run
47 |
48 | ```bash
49 | git clone https://github.com/dai-shi/will-this-react-global-state-work-in-concurrent-rendering.git
50 | cd will-this-react-global-state-work-in-concurrent-rendering
51 | yarn install
52 | yarn run build-all
53 | yarn run jest
54 | ```
55 |
56 | To automatically run tests and update the README.md on OSX:
57 | ```
58 | yarn jest:json
59 | yarn jest:update
60 | ```
61 |
62 | ## Screencast (old one with react-redux v7. v8 works great.)
63 |
64 |
65 |
66 | ## Test scenario
67 |
68 | - With useTransition
69 | - Level 1
70 | - 1: No tearing finally on update
71 | - 2: No tearing finally on mount
72 | - Level 2
73 | - 3: No tearing temporarily on update
74 | - 4: No tearing temporarily on mount
75 | - Level 3
76 | - 5: Can interrupt render (time slicing)
77 | - 6: Can branch state (wip state)
78 | - With useDeferredValue
79 | - Level 1
80 | - 7: No tearing finally on update
81 | - 8: No tearing finally on mount
82 | - Level 2
83 | - 9: No tearing temporarily on update
84 | - 10: No tearing temporarily on mount
85 |
86 | ## Results
87 |
88 |
89 | Raw Output
90 |
91 | ```
92 | With useTransition
93 | Level 1
94 | ✓ No tearing finally on update (7990 ms)
95 | ✓ No tearing finally on mount (4608 ms)
96 | Level 2
97 | ✓ No tearing temporarily on update (12955 ms)
98 | ✓ No tearing temporarily on mount (4546 ms)
99 | Level 3
100 | ✕ Can interrupt render (time slicing) (7926 ms)
101 | ✕ Can branch state (wip state) (6655 ms)
102 | With useDeferredValue
103 | Level 1
104 | ✓ No tearing finally on update (9568 ms)
105 | ✓ No tearing finally on mount (4544 ms)
106 | Level 2
107 | ✓ No tearing temporarily on update (14627 ms)
108 | ✓ No tearing temporarily on mount (4569 ms)
109 | zustand
110 | With useTransition
111 | Level 1
112 | ✓ No tearing finally on update (7994 ms)
113 | ✓ No tearing finally on mount (4623 ms)
114 | Level 2
115 | ✓ No tearing temporarily on update (12966 ms)
116 | ✓ No tearing temporarily on mount (4505 ms)
117 | Level 3
118 | ✕ Can interrupt render (time slicing) (7922 ms)
119 | ✕ Can branch state (wip state) (6679 ms)
120 | With useDeferredValue
121 | Level 1
122 | ✓ No tearing finally on update (9631 ms)
123 | ✓ No tearing finally on mount (4641 ms)
124 | Level 2
125 | ✓ No tearing temporarily on update (14656 ms)
126 | ✓ No tearing temporarily on mount (4544 ms)
127 | react-tracked
128 | With useTransition
129 | Level 1
130 | ✓ No tearing finally on update (5586 ms)
131 | ✓ No tearing finally on mount (9520 ms)
132 | Level 2
133 | ✓ No tearing temporarily on update (8625 ms)
134 | ✓ No tearing temporarily on mount (9455 ms)
135 | Level 3
136 | ✓ Can interrupt render (time slicing) (3555 ms)
137 | ✓ Can branch state (wip state) (8216 ms)
138 | With useDeferredValue
139 | Level 1
140 | ✓ No tearing finally on update (15399 ms)
141 | ✓ No tearing finally on mount (6528 ms)
142 | Level 2
143 | ✓ No tearing temporarily on update (19473 ms)
144 | ✓ No tearing temporarily on mount (8479 ms)
145 | constate
146 | With useTransition
147 | Level 1
148 | ✓ No tearing finally on update (4526 ms)
149 | ✓ No tearing finally on mount (7464 ms)
150 | Level 2
151 | ✓ No tearing temporarily on update (8619 ms)
152 | ✓ No tearing temporarily on mount (8491 ms)
153 | Level 3
154 | ✓ Can interrupt render (time slicing) (3635 ms)
155 | ✓ Can branch state (wip state) (5159 ms)
156 | With useDeferredValue
157 | Level 1
158 | ✓ No tearing finally on update (9626 ms)
159 | ✓ No tearing finally on mount (6629 ms)
160 | Level 2
161 | ✓ No tearing temporarily on update (14643 ms)
162 | ✓ No tearing temporarily on mount (5578 ms)
163 | react-hooks-global-state
164 | With useTransition
165 | Level 1
166 | ✓ No tearing finally on update (7954 ms)
167 | ✓ No tearing finally on mount (4564 ms)
168 | Level 2
169 | ✓ No tearing temporarily on update (12975 ms)
170 | ✓ No tearing temporarily on mount (4525 ms)
171 | Level 3
172 | ✕ Can interrupt render (time slicing) (7896 ms)
173 | ✕ Can branch state (wip state) (6648 ms)
174 | With useDeferredValue
175 | Level 1
176 | ✓ No tearing finally on update (9624 ms)
177 | ✓ No tearing finally on mount (4547 ms)
178 | Level 2
179 | ✓ No tearing temporarily on update (14636 ms)
180 | ✓ No tearing temporarily on mount (4549 ms)
181 | use-context-selector-base
182 | With useTransition
183 | Level 1
184 | ✓ No tearing finally on update (7851 ms)
185 | ✓ No tearing finally on mount (8476 ms)
186 | Level 2
187 | ✓ No tearing temporarily on update (12836 ms)
188 | ✓ No tearing temporarily on mount (8496 ms)
189 | Level 3
190 | ✕ Can interrupt render (time slicing) (7846 ms)
191 | ✕ Can branch state (wip state) (7629 ms)
192 | With useDeferredValue
193 | Level 1
194 | ✓ No tearing finally on update (9706 ms)
195 | ✓ No tearing finally on mount (5650 ms)
196 | Level 2
197 | ✓ No tearing temporarily on update (14623 ms)
198 | ✓ No tearing temporarily on mount (5590 ms)
199 | use-context-selector
200 | With useTransition
201 | Level 1
202 | ✓ No tearing finally on update (5503 ms)
203 | ✓ No tearing finally on mount (11504 ms)
204 | Level 2
205 | ✓ No tearing temporarily on update (8629 ms)
206 | ✓ No tearing temporarily on mount (11478 ms)
207 | Level 3
208 | ✓ Can interrupt render (time slicing) (3565 ms)
209 | ✓ Can branch state (wip state) (8202 ms)
210 | With useDeferredValue
211 | Level 1
212 | ✓ No tearing finally on update (15341 ms)
213 | ✓ No tearing finally on mount (6542 ms)
214 | Level 2
215 | ✓ No tearing temporarily on update (20063 ms)
216 | ✓ No tearing temporarily on mount (8598 ms)
217 | use-subscription
218 | With useTransition
219 | Level 1
220 | ✓ No tearing finally on update (7989 ms)
221 | ✓ No tearing finally on mount (4610 ms)
222 | Level 2
223 | ✓ No tearing temporarily on update (12955 ms)
224 | ✓ No tearing temporarily on mount (4541 ms)
225 | Level 3
226 | ✕ Can interrupt render (time slicing) (7947 ms)
227 | ✕ Can branch state (wip state) (6656 ms)
228 | With useDeferredValue
229 | Level 1
230 | ✓ No tearing finally on update (9612 ms)
231 | ✓ No tearing finally on mount (4555 ms)
232 | Level 2
233 | ✓ No tearing temporarily on update (14580 ms)
234 | ✓ No tearing temporarily on mount (4588 ms)
235 | apollo-client
236 | With useTransition
237 | Level 1
238 | ✓ No tearing finally on update (8142 ms)
239 | ✓ No tearing finally on mount (4638 ms)
240 | Level 2
241 | ✓ No tearing temporarily on update (13105 ms)
242 | ✓ No tearing temporarily on mount (5551 ms)
243 | Level 3
244 | ✕ Can interrupt render (time slicing) (8083 ms)
245 | ✕ Can branch state (wip state) (7756 ms)
246 | With useDeferredValue
247 | Level 1
248 | ✓ No tearing finally on update (6514 ms)
249 | ✓ No tearing finally on mount (5679 ms)
250 | Level 2
251 | ✓ No tearing temporarily on update (9692 ms)
252 | ✓ No tearing temporarily on mount (4724 ms)
253 | recoil
254 | With useTransition
255 | Level 1
256 | ✓ No tearing finally on update (8119 ms)
257 | ✓ No tearing finally on mount (4729 ms)
258 | Level 2
259 | ✓ No tearing temporarily on update (13109 ms)
260 | ✓ No tearing temporarily on mount (4670 ms)
261 | Level 3
262 | ✕ Can interrupt render (time slicing) (8047 ms)
263 | ✕ Can branch state (wip state) (6808 ms)
264 | With useDeferredValue
265 | Level 1
266 | ✓ No tearing finally on update (9780 ms)
267 | ✓ No tearing finally on mount (4673 ms)
268 | Level 2
269 | ✓ No tearing temporarily on update (14784 ms)
270 | ✓ No tearing temporarily on mount (4667 ms)
271 | recoil_UNSTABLE
272 | With useTransition
273 | Level 1
274 | ✓ No tearing finally on update (5736 ms)
275 | ✓ No tearing finally on mount (5624 ms)
276 | Level 2
277 | ✓ No tearing temporarily on update (8723 ms)
278 | ✕ No tearing temporarily on mount (5586 ms)
279 | Level 3
280 | ✓ Can interrupt render (time slicing) (3763 ms)
281 | ✕ Can branch state (wip state) (10277 ms)
282 | With useDeferredValue
283 | Level 1
284 | ✓ No tearing finally on update (11399 ms)
285 | ✓ No tearing finally on mount (5612 ms)
286 | Level 2
287 | ✓ No tearing temporarily on update (15529 ms)
288 | ✕ No tearing temporarily on mount (5579 ms)
289 | jotai
290 | With useTransition
291 | Level 1
292 | ✓ No tearing finally on update (5633 ms)
293 | ✓ No tearing finally on mount (6580 ms)
294 | Level 2
295 | ✓ No tearing temporarily on update (9753 ms)
296 | ✕ No tearing temporarily on mount (6550 ms)
297 | Level 3
298 | ✓ Can interrupt render (time slicing) (4707 ms)
299 | ✕ Can branch state (wip state) (10238 ms)
300 | With useDeferredValue
301 | Level 1
302 | ✓ No tearing finally on update (10713 ms)
303 | ✓ No tearing finally on mount (6736 ms)
304 | Level 2
305 | ✓ No tearing temporarily on update (15726 ms)
306 | ✕ No tearing temporarily on mount (5661 ms)
307 | use-atom
308 | With useTransition
309 | Level 1
310 | ✓ No tearing finally on update (6616 ms)
311 | ✓ No tearing finally on mount (9592 ms)
312 | Level 2
313 | ✓ No tearing temporarily on update (9713 ms)
314 | ✓ No tearing temporarily on mount (9559 ms)
315 | Level 3
316 | ✓ Can interrupt render (time slicing) (4749 ms)
317 | ✓ Can branch state (wip state) (9292 ms)
318 | With useDeferredValue
319 | Level 1
320 | ✓ No tearing finally on update (16565 ms)
321 | ✓ No tearing finally on mount (6647 ms)
322 | Level 2
323 | ✓ No tearing temporarily on update (20596 ms)
324 | ✓ No tearing temporarily on mount (6604 ms)
325 | valtio
326 | With useTransition
327 | Level 1
328 | ✓ No tearing finally on update (8087 ms)
329 | ✓ No tearing finally on mount (4701 ms)
330 | Level 2
331 | ✓ No tearing temporarily on update (13031 ms)
332 | ✓ No tearing temporarily on mount (4741 ms)
333 | Level 3
334 | ✕ Can interrupt render (time slicing) (8028 ms)
335 | ✕ Can branch state (wip state) (6785 ms)
336 | With useDeferredValue
337 | Level 1
338 | ✓ No tearing finally on update (9729 ms)
339 | ✓ No tearing finally on mount (4694 ms)
340 | Level 2
341 | ✓ No tearing temporarily on update (14789 ms)
342 | ✓ No tearing temporarily on mount (4682 ms)
343 | effector
344 | With useTransition
345 | Level 1
346 | ✓ No tearing finally on update (8153 ms)
347 | ✓ No tearing finally on mount (4653 ms)
348 | Level 2
349 | ✓ No tearing temporarily on update (13080 ms)
350 | ✓ No tearing temporarily on mount (4668 ms)
351 | Level 3
352 | ✕ Can interrupt render (time slicing) (8003 ms)
353 | ✕ Can branch state (wip state) (6776 ms)
354 | With useDeferredValue
355 | Level 1
356 | ✓ No tearing finally on update (9689 ms)
357 | ✓ No tearing finally on mount (4730 ms)
358 | Level 2
359 | ✓ No tearing temporarily on update (14725 ms)
360 | ✓ No tearing temporarily on mount (4608 ms)
361 | react-rxjs
362 | With useTransition
363 | Level 1
364 | ✓ No tearing finally on update (8066 ms)
365 | ✓ No tearing finally on mount (4658 ms)
366 | Level 2
367 | ✓ No tearing temporarily on update (13040 ms)
368 | ✓ No tearing temporarily on mount (4637 ms)
369 | Level 3
370 | ✕ Can interrupt render (time slicing) (8027 ms)
371 | ✕ Can branch state (wip state) (6797 ms)
372 | With useDeferredValue
373 | Level 1
374 | ✓ No tearing finally on update (9765 ms)
375 | ✓ No tearing finally on mount (4625 ms)
376 | Level 2
377 | ✓ No tearing temporarily on update (14783 ms)
378 | ✓ No tearing temporarily on mount (4642 ms)
379 | simplux
380 | With useTransition
381 | Level 1
382 | ✓ No tearing finally on update (4613 ms)
383 | ✓ No tearing finally on mount (8591 ms)
384 | Level 2
385 | ✓ No tearing temporarily on update (8730 ms)
386 | ✓ No tearing temporarily on mount (8572 ms)
387 | Level 3
388 | ✓ Can interrupt render (time slicing) (3712 ms)
389 | ✕ Can branch state (wip state) (9293 ms)
390 | With useDeferredValue
391 | Level 1
392 | ✓ No tearing finally on update (9718 ms)
393 | ✓ No tearing finally on mount (6708 ms)
394 | Level 2
395 | ✓ No tearing temporarily on update (14698 ms)
396 | ✓ No tearing temporarily on mount (5680 ms)
397 | react-query
398 | With useTransition
399 | Level 1
400 | ✓ No tearing finally on update (8131 ms)
401 | ✓ No tearing finally on mount (4716 ms)
402 | Level 2
403 | ✕ No tearing temporarily on update (13174 ms)
404 | ✓ No tearing temporarily on mount (4655 ms)
405 | Level 3
406 | ✕ Can interrupt render (time slicing) (8120 ms)
407 | ✕ Can branch state (wip state) (6807 ms)
408 | With useDeferredValue
409 | Level 1
410 | ✓ No tearing finally on update (9594 ms)
411 | ✓ No tearing finally on mount (4665 ms)
412 | Level 2
413 | ✓ No tearing temporarily on update (13721 ms)
414 | ✓ No tearing temporarily on mount (4653 ms)
415 | mobx-react-lite
416 | With useTransition
417 | Level 1
418 | ✓ No tearing finally on update (4651 ms)
419 | ✓ No tearing finally on mount (5610 ms)
420 | Level 2
421 | ✓ No tearing temporarily on update (8739 ms)
422 | ✓ No tearing temporarily on mount (6586 ms)
423 | Level 3
424 | ✕ Can interrupt render (time slicing) (3692 ms)
425 | ✕ Can branch state (wip state) (3071 ms)
426 | With useDeferredValue
427 | Level 1
428 | ✓ No tearing finally on update (9777 ms)
429 | ✓ No tearing finally on mount (6595 ms)
430 | Level 2
431 | ✓ No tearing temporarily on update (14724 ms)
432 | ✓ No tearing temporarily on mount (6568 ms)
433 |
434 | ```
435 |
436 |
437 |
438 | Test | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
439 |
440 | react-redux |
441 | :white_check_mark: |
442 | :white_check_mark: |
443 | :white_check_mark: |
444 | :white_check_mark: |
445 | :x: |
446 | :x: |
447 | :white_check_mark: |
448 | :white_check_mark: |
449 | :white_check_mark: |
450 | :white_check_mark: |
451 |
452 |
453 | zustand |
454 | :white_check_mark: |
455 | :white_check_mark: |
456 | :white_check_mark: |
457 | :white_check_mark: |
458 | :x: |
459 | :x: |
460 | :white_check_mark: |
461 | :white_check_mark: |
462 | :white_check_mark: |
463 | :white_check_mark: |
464 |
465 |
466 | react-tracked |
467 | :white_check_mark: |
468 | :white_check_mark: |
469 | :white_check_mark: |
470 | :white_check_mark: |
471 | :white_check_mark: |
472 | :white_check_mark: |
473 | :white_check_mark: |
474 | :white_check_mark: |
475 | :white_check_mark: |
476 | :white_check_mark: |
477 |
478 |
479 | constate |
480 | :white_check_mark: |
481 | :white_check_mark: |
482 | :white_check_mark: |
483 | :white_check_mark: |
484 | :white_check_mark: |
485 | :white_check_mark: |
486 | :white_check_mark: |
487 | :white_check_mark: |
488 | :white_check_mark: |
489 | :white_check_mark: |
490 |
491 |
492 | react-hooks-global-state |
493 | :white_check_mark: |
494 | :white_check_mark: |
495 | :white_check_mark: |
496 | :white_check_mark: |
497 | :x: |
498 | :x: |
499 | :white_check_mark: |
500 | :white_check_mark: |
501 | :white_check_mark: |
502 | :white_check_mark: |
503 |
504 |
505 | use-context-selector (w/ useReducer, w/o useContextUpdate) |
506 | :white_check_mark: |
507 | :white_check_mark: |
508 | :white_check_mark: |
509 | :white_check_mark: |
510 | :x: |
511 | :x: |
512 | :white_check_mark: |
513 | :white_check_mark: |
514 | :white_check_mark: |
515 | :white_check_mark: |
516 |
517 |
518 | use-context-selector (w/ useReducer) |
519 | :white_check_mark: |
520 | :white_check_mark: |
521 | :white_check_mark: |
522 | :white_check_mark: |
523 | :white_check_mark: |
524 | :white_check_mark: |
525 | :white_check_mark: |
526 | :white_check_mark: |
527 | :white_check_mark: |
528 | :white_check_mark: |
529 |
530 |
531 | use-subscription (w/ redux) |
532 | :white_check_mark: |
533 | :white_check_mark: |
534 | :white_check_mark: |
535 | :white_check_mark: |
536 | :x: |
537 | :x: |
538 | :white_check_mark: |
539 | :white_check_mark: |
540 | :white_check_mark: |
541 | :white_check_mark: |
542 |
543 |
544 | apollo-client |
545 | :white_check_mark: |
546 | :white_check_mark: |
547 | :white_check_mark: |
548 | :white_check_mark: |
549 | :x: |
550 | :x: |
551 | :white_check_mark: |
552 | :white_check_mark: |
553 | :white_check_mark: |
554 | :white_check_mark: |
555 |
556 |
557 | recoil |
558 | :white_check_mark: |
559 | :white_check_mark: |
560 | :white_check_mark: |
561 | :white_check_mark: |
562 | :x: |
563 | :x: |
564 | :white_check_mark: |
565 | :white_check_mark: |
566 | :white_check_mark: |
567 | :white_check_mark: |
568 |
569 |
570 | recoil (UNSTABLE) |
571 | :white_check_mark: |
572 | :white_check_mark: |
573 | :white_check_mark: |
574 | :x: |
575 | :white_check_mark: |
576 | :x: |
577 | :white_check_mark: |
578 | :white_check_mark: |
579 | :white_check_mark: |
580 | :x: |
581 |
582 |
583 | jotai |
584 | :white_check_mark: |
585 | :white_check_mark: |
586 | :white_check_mark: |
587 | :x: |
588 | :white_check_mark: |
589 | :x: |
590 | :white_check_mark: |
591 | :white_check_mark: |
592 | :white_check_mark: |
593 | :x: |
594 |
595 |
596 | use-atom |
597 | :white_check_mark: |
598 | :white_check_mark: |
599 | :white_check_mark: |
600 | :white_check_mark: |
601 | :white_check_mark: |
602 | :white_check_mark: |
603 | :white_check_mark: |
604 | :white_check_mark: |
605 | :white_check_mark: |
606 | :white_check_mark: |
607 |
608 |
609 | valtio |
610 | :white_check_mark: |
611 | :white_check_mark: |
612 | :white_check_mark: |
613 | :white_check_mark: |
614 | :x: |
615 | :x: |
616 | :white_check_mark: |
617 | :white_check_mark: |
618 | :white_check_mark: |
619 | :white_check_mark: |
620 |
621 |
622 | effector |
623 | :white_check_mark: |
624 | :white_check_mark: |
625 | :white_check_mark: |
626 | :white_check_mark: |
627 | :x: |
628 | :x: |
629 | :white_check_mark: |
630 | :white_check_mark: |
631 | :white_check_mark: |
632 | :white_check_mark: |
633 |
634 |
635 | react-rxjs |
636 | :white_check_mark: |
637 | :white_check_mark: |
638 | :white_check_mark: |
639 | :white_check_mark: |
640 | :x: |
641 | :x: |
642 | :white_check_mark: |
643 | :white_check_mark: |
644 | :white_check_mark: |
645 | :white_check_mark: |
646 |
647 |
648 | simplux |
649 | :white_check_mark: |
650 | :white_check_mark: |
651 | :white_check_mark: |
652 | :white_check_mark: |
653 | :white_check_mark: |
654 | :x: |
655 | :white_check_mark: |
656 | :white_check_mark: |
657 | :white_check_mark: |
658 | :white_check_mark: |
659 |
660 |
661 | react-query |
662 | :white_check_mark: |
663 | :white_check_mark: |
664 | :x: |
665 | :white_check_mark: |
666 | :x: |
667 | :x: |
668 | :white_check_mark: |
669 | :white_check_mark: |
670 | :white_check_mark: |
671 | :white_check_mark: |
672 |
673 |
674 | mobx-react-lite |
675 | :white_check_mark: |
676 | :white_check_mark: |
677 | :white_check_mark: |
678 | :white_check_mark: |
679 | :x: |
680 | :x: |
681 | :white_check_mark: |
682 | :white_check_mark: |
683 | :white_check_mark: |
684 | :white_check_mark: |
685 |
686 |
687 |
688 |
689 | ## Caveats
690 |
691 | - Tearing and state branching may not be an issue depending on app requirements.
692 | - The test is done in a very limited way.
693 | - Passing tests don't guarantee anything.
694 | - The results may not be accurate.
695 | - Do not fully trust the results.
696 |
697 | ## If you are interested
698 |
699 | The reason why I created this is to test my projects.
700 |
701 | - [react-tracked](https://github.com/dai-shi/react-tracked)
702 | - [use-context-selector](https://github.com/dai-shi/use-context-selector)
703 | - and so on
704 |
705 | ## Contributing
706 |
707 | This repository is a tool for us to test some of global state libraries.
708 | While it is totally fine to use the tool for other libraries under the license,
709 | we don't generally accept adding a new library to the repository.
710 |
711 | However, we are interested in various approaches.
712 | If you have any suggestions feel free to open issues or pull requests.
713 | We may consider adding (and removing) libraries.
714 | Questions and discussions are also welcome in issues.
715 |
716 | For listing global state libraries, we have another repository
717 | https://github.com/dai-shi/lets-compare-global-state-with-react-hooks
718 | in which we accept contributions. It's recommended to run this tool
719 | and we put the result there, possibly a reference link to a PR
720 | in this repository or a fork of this repository.
721 |
--------------------------------------------------------------------------------
/__tests__/all_spec.js:
--------------------------------------------------------------------------------
1 | /* global page, jestPuppeteer */
2 |
3 | import { NUM_CHILD_COMPONENTS } from '../src/common';
4 |
5 | const NUM_COMPONENTS = NUM_CHILD_COMPONENTS + 1; // plus one in
6 | const ids = [...Array(NUM_COMPONENTS).keys()];
7 | const REPEAT = 5;
8 |
9 | const port = process.env.PORT || '8080';
10 |
11 | const sleep = (ms) => new Promise((r) => {
12 | setTimeout(r, ms);
13 | });
14 | jest.setTimeout(20 * 1000);
15 |
16 | const names = [
17 | // 'react-state',
18 | 'react-redux',
19 | 'zustand',
20 | 'react-tracked',
21 | 'constate',
22 | 'react-hooks-global-state',
23 | 'use-context-selector-base',
24 | 'use-context-selector',
25 | 'use-subscription',
26 | 'apollo-client',
27 | 'recoil',
28 | 'recoil_UNSTABLE',
29 | 'jotai',
30 | 'use-atom',
31 | 'valtio',
32 | 'effector',
33 | 'react-rxjs',
34 | 'simplux',
35 | 'react-query',
36 | 'mobx-react-lite',
37 | ];
38 |
39 | names.forEach((name) => {
40 | describe(name, () => {
41 | beforeEach(async () => {
42 | await page.goto(`http://localhost:${port}/${name}/index.html`);
43 | await sleep(1000); // to make it stable
44 | });
45 |
46 | afterEach(async () => {
47 | await jestPuppeteer.resetBrowser();
48 | });
49 |
50 | describe('With useTransition', () => {
51 | describe('Level 1', () => {
52 | it('No tearing finally on update', async () => {
53 | await page.click('#transitionShowCounter');
54 | // wait until all counts become zero
55 | await Promise.all(ids.map(async (i) => {
56 | await expect(page).toMatchElement(`.count:nth-of-type(${i + 1})`, {
57 | text: '0',
58 | timeout: 5 * 1000,
59 | });
60 | }));
61 | for (let loop = 0; loop < REPEAT; loop += 1) {
62 | await page.click('#transitionIncrement');
63 | await sleep(100);
64 | }
65 | // check if all counts become REPEAT
66 | await Promise.all(ids.map(async (i) => {
67 | await expect(page).toMatchElement(`.count:nth-of-type(${i + 1})`, {
68 | text: `${REPEAT}`,
69 | timeout: 10 * 1000,
70 | });
71 | }));
72 | });
73 |
74 | it('No tearing finally on mount', async () => {
75 | await page.click('#startAutoIncrement');
76 | await sleep(100);
77 | await page.click('#transitionShowCounter');
78 | await sleep(1000);
79 | await page.click('#stopAutoIncrement');
80 | await sleep(2000);
81 | const count = page.evaluate(() => document.querySelector('.count:first-of-type'));
82 | await Promise.all(ids.map(async (i) => {
83 | await expect(page).toMatchElement(`.count:nth-of-type(${i + 1})`, {
84 | text: count,
85 | timeout: 10 * 1000,
86 | });
87 | }));
88 | });
89 | });
90 |
91 | describe('Level 2', () => {
92 | it('No tearing temporarily on update', async () => {
93 | await page.click('#transitionShowCounter');
94 | // wait until all counts become zero
95 | await Promise.all(ids.map(async (i) => {
96 | await expect(page).toMatchElement(`.count:nth-of-type(${i + 1})`, {
97 | text: '0',
98 | timeout: 5 * 1000,
99 | });
100 | }));
101 | for (let loop = 0; loop < REPEAT; loop += 1) {
102 | await page.click('#transitionIncrement');
103 | await sleep(100);
104 | }
105 | await sleep(5000);
106 | // check if there's inconsistency during update
107 | // see useCheckTearing() in src/common.js
108 | await expect(page.title()).resolves.not.toMatch(/TEARED/);
109 | });
110 |
111 | it('No tearing temporarily on mount', async () => {
112 | await page.click('#startAutoIncrement');
113 | await sleep(100);
114 | await page.click('#transitionShowCounter');
115 | await sleep(1000);
116 | await page.click('#stopAutoIncrement');
117 | await sleep(2000);
118 | // check if there's inconsistency during update
119 | // see useCheckTearing() in src/common.js
120 | await expect(page.title()).resolves.not.toMatch(/TEARED/);
121 | });
122 | });
123 |
124 | describe('Level 3', () => {
125 | it('Can interrupt render (time slicing)', async () => {
126 | await page.click('#transitionShowCounter');
127 | // wait until all counts become zero
128 | await Promise.all(ids.map(async (i) => {
129 | await expect(page).toMatchElement(`.count:nth-of-type(${i + 1})`, {
130 | text: '0',
131 | timeout: 5 * 1000,
132 | });
133 | }));
134 | const delays = [];
135 | for (let loop = 0; loop < REPEAT; loop += 1) {
136 | const start = Date.now();
137 | await page.click('#transitionIncrement');
138 | delays.push(Date.now() - start);
139 | await sleep(100);
140 | }
141 | console.log(name, delays);
142 | // check delays taken by clicking buttons in check1
143 | // each render takes at least 20ms and there are 50 components,
144 | // it triggers triple clicks, so 300ms on average.
145 | const avg = delays.reduce((a, b) => a + b) / delays.length;
146 | expect(avg).toBeLessThan(300);
147 | });
148 |
149 | it('Can branch state (wip state)', async () => {
150 | await page.click('#transitionShowCounter');
151 | await page.click('#transitionIncrement');
152 | await Promise.all(ids.map(async (i) => {
153 | await expect(page).toMatchElement(`.count:nth-of-type(${i + 1})`, {
154 | text: '1',
155 | timeout: 5 * 1000,
156 | });
157 | }));
158 | await page.click('#transitionIncrement');
159 | await sleep(100);
160 | await page.click('#transitionIncrement');
161 | // wait for pending
162 | await expect(page).toMatchElement('#pending', {
163 | text: 'Pending...',
164 | timeout: 2 * 1000,
165 | });
166 | // Make sure that while isPending true, previous state displayed
167 | await expect(page.evaluate(() => document.querySelector('#mainCount').innerHTML)).resolves.toBe('1');
168 | await expect(page.evaluate(() => document.querySelector('.count:first-of-type').innerHTML)).resolves.toBe('1');
169 | // click normal double button
170 | await page.click('#normalDouble');
171 | // check if all counts become doubled before increment
172 | await Promise.all(ids.map(async (i) => {
173 | await expect(page).toMatchElement(`.count:nth-of-type(${i + 1})`, {
174 | text: '2',
175 | timeout: 5 * 1000,
176 | });
177 | }));
178 | // check if all counts become doubled after increment
179 | await Promise.all(ids.map(async (i) => {
180 | await expect(page).toMatchElement(`.count:nth-of-type(${i + 1})`, {
181 | text: '6',
182 | timeout: 5 * 1000,
183 | });
184 | }));
185 | });
186 | });
187 | });
188 |
189 | describe('With useDeferredValue', () => {
190 | describe('Level 1', () => {
191 | it('No tearing finally on update', async () => {
192 | await page.click('#transitionShowDeferred');
193 | // wait until all counts become zero
194 | await Promise.all(ids.map(async (i) => {
195 | await expect(page).toMatchElement(`.count:nth-of-type(${i + 1})`, {
196 | text: '0',
197 | timeout: 5 * 1000,
198 | });
199 | }));
200 | for (let loop = 0; loop < REPEAT; loop += 1) {
201 | await page.click('#normalIncrement');
202 | await sleep(100);
203 | }
204 | // check if all counts become REPEAT
205 | await Promise.all(ids.map(async (i) => {
206 | await expect(page).toMatchElement(`.count:nth-of-type(${i + 1})`, {
207 | text: `${REPEAT}`,
208 | timeout: 10 * 1000,
209 | });
210 | }));
211 | });
212 |
213 | it('No tearing finally on mount', async () => {
214 | await page.click('#startAutoIncrement');
215 | await sleep(100);
216 | await page.click('#transitionShowDeferred');
217 | await sleep(1000);
218 | await page.click('#stopAutoIncrement');
219 | await sleep(2000);
220 | const count = page.evaluate(() => document.querySelector('.count:first-of-type'));
221 | await Promise.all(ids.map(async (i) => {
222 | await expect(page).toMatchElement(`.count:nth-of-type(${i + 1})`, {
223 | text: count,
224 | timeout: 10 * 1000,
225 | });
226 | }));
227 | });
228 | });
229 |
230 | describe('Level 2', () => {
231 | it('No tearing temporarily on update', async () => {
232 | await page.click('#transitionShowDeferred');
233 | // wait until all counts become zero
234 | await Promise.all(ids.map(async (i) => {
235 | await expect(page).toMatchElement(`.count:nth-of-type(${i + 1})`, {
236 | text: '0',
237 | timeout: 5 * 1000,
238 | });
239 | }));
240 | for (let loop = 0; loop < REPEAT; loop += 1) {
241 | await page.click('#normalIncrement');
242 | await sleep(100);
243 | }
244 | await sleep(5000);
245 | // check if there's inconsistency during update
246 | // see useCheckTearing() in src/common.js
247 | await expect(page.title()).resolves.not.toMatch(/TEARED/);
248 | });
249 |
250 | it('No tearing temporarily on mount', async () => {
251 | await page.click('#startAutoIncrement');
252 | await sleep(100);
253 | await page.click('#transitionShowDeferred');
254 | await sleep(1000);
255 | await page.click('#stopAutoIncrement');
256 | await sleep(2000);
257 | // check if there's inconsistency during update
258 | // see useCheckTearing() in src/common.js
259 | await expect(page.title()).resolves.not.toMatch(/TEARED/);
260 | });
261 | });
262 | });
263 | });
264 | });
265 |
--------------------------------------------------------------------------------
/jest-puppeteer.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | launch: {
3 | headless: false,
4 | },
5 | server: {
6 | command: 'http-server dist',
7 | port: 8080,
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "will-this-react-global-state-work-in-concurrent-rendering",
3 | "description": "Test tearing and branching in React concurrent rendering",
4 | "private": true,
5 | "version": "0.2.0",
6 | "author": "Daishi Kato",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/dai-shi/will-this-react-global-state-work-in-concurrent-rendering.git"
10 | },
11 | "scripts": {
12 | "test": "run-s eslint",
13 | "eslint": "eslint --ignore-pattern dist .",
14 | "jest": "cross-env BABEL_ENV=jest jest",
15 | "jest:json": "cross-env BABEL_ENV=jest jest --json --outputFile=./outfile.json --no-color 2> ./outfile_raw.txt",
16 | "jest:update": "node update_readme.js",
17 | "dev-server": "webpack serve --mode=development",
18 | "http-server": "http-server dist",
19 | "build:react-redux": "cross-env NAME=react-redux webpack",
20 | "build:react-tracked": "cross-env NAME=react-tracked webpack",
21 | "build:react-query": "cross-env NAME=react-query webpack",
22 | "build:constate": "cross-env NAME=constate webpack",
23 | "build:zustand": "cross-env NAME=zustand webpack",
24 | "build:react-hooks-global-state": "cross-env NAME=react-hooks-global-state webpack",
25 | "build:use-context-selector-base": "cross-env NAME=use-context-selector-base webpack",
26 | "build:use-context-selector": "cross-env NAME=use-context-selector webpack",
27 | "build:use-subscription": "cross-env NAME=use-subscription webpack",
28 | "build:react-state": "cross-env NAME=react-state webpack",
29 | "build:simplux": "cross-env NAME=simplux webpack",
30 | "build:apollo-client": "cross-env NAME=apollo-client webpack",
31 | "build:recoil": "cross-env NAME=recoil webpack",
32 | "build:recoil_UNSTABLE": "cross-env NAME=recoil_UNSTABLE webpack",
33 | "build:jotai": "cross-env NAME=jotai webpack",
34 | "build:use-atom": "cross-env NAME=use-atom webpack",
35 | "build:effector": "cross-env NAME=effector webpack",
36 | "build:react-rxjs": "cross-env NAME=react-rxjs webpack",
37 | "build:valtio": "cross-env NAME=valtio webpack",
38 | "build:mobx-react-lite": "cross-env NAME=mobx-react-lite webpack",
39 | "build-all": "run-s build:*"
40 | },
41 | "keywords": [
42 | "react",
43 | "context",
44 | "hooks"
45 | ],
46 | "license": "MIT",
47 | "dependencies": {
48 | "@apollo/client": "^3.7.6",
49 | "@react-rxjs/core": "^0.10.3",
50 | "@simplux/core": "^0.18.0",
51 | "@simplux/react": "^0.18.0",
52 | "constate": "^3.3.2",
53 | "effector": "^22.5.0",
54 | "effector-react": "^22.4.0",
55 | "graphql": "^16.6.0",
56 | "jotai": "^2.0.0",
57 | "mobx": "^6.10.2",
58 | "mobx-react-lite": "^4.0.4",
59 | "react": "^18.2.0",
60 | "react-dom": "^18.2.0",
61 | "react-hooks-global-state": "^2.1.0",
62 | "react-query": "^4.0.0-beta.3",
63 | "react-redux": "^8.0.5",
64 | "react-tracked": "^1.7.11",
65 | "recoil": "^0.7.6",
66 | "redux": "^4.2.1",
67 | "rxjs": "^7.8.0",
68 | "use-atom": "^0.9.0",
69 | "use-context-selector": "^1.4.1",
70 | "use-subscription": "^1.8.0",
71 | "valtio": "^1.9.0",
72 | "zustand": "^4.3.2"
73 | },
74 | "devDependencies": {
75 | "@babel/cli": "^7.20.7",
76 | "@babel/core": "^7.20.12",
77 | "@babel/preset-env": "^7.20.2",
78 | "@babel/preset-react": "^7.18.6",
79 | "babel-loader": "^9.1.2",
80 | "cross-env": "^7.0.3",
81 | "eslint": "^8.33.0",
82 | "eslint-config-airbnb": "^19.0.4",
83 | "eslint-plugin-import": "^2.27.5",
84 | "eslint-plugin-jsx-a11y": "^6.7.1",
85 | "eslint-plugin-react": "^7.32.2",
86 | "eslint-plugin-react-hooks": "^4.6.0",
87 | "html-webpack-plugin": "^5.5.0",
88 | "http-server": "^14.1.1",
89 | "jest": "^29.4.1",
90 | "jest-puppeteer": "^6.2.0",
91 | "npm-run-all": "^4.1.5",
92 | "puppeteer": "17.1.3",
93 | "webpack": "^5.75.0",
94 | "webpack-cli": "^5.0.1",
95 | "webpack-dev-server": "^4.11.1"
96 | },
97 | "babel": {
98 | "env": {
99 | "development": {
100 | "presets": [
101 | [
102 | "@babel/preset-env",
103 | {
104 | "targets": "> 0.2%, not dead"
105 | }
106 | ],
107 | "@babel/preset-react"
108 | ]
109 | },
110 | "jest": {
111 | "plugins": [
112 | "@babel/plugin-transform-modules-commonjs",
113 | "@babel/plugin-transform-react-jsx"
114 | ]
115 | }
116 | }
117 | },
118 | "jest": {
119 | "preset": "jest-puppeteer"
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | dev
5 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/apollo-client/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | ApolloClient,
4 | gql,
5 | InMemoryCache,
6 | ApolloProvider,
7 | useQuery,
8 | makeVar,
9 | } from '@apollo/client';
10 |
11 | import {
12 | reducer,
13 | initialState,
14 | selectCount,
15 | incrementAction,
16 | doubleAction,
17 | createApp,
18 | } from '../common';
19 |
20 | const COUNT_QUERY = gql`
21 | query CountQuery {
22 | count @client
23 | }
24 | `;
25 |
26 | const currentState = makeVar(initialState);
27 |
28 | const client = new ApolloClient({
29 | cache: new InMemoryCache({
30 | typePolicies: {
31 | Query: {
32 | fields: {
33 | count() {
34 | return currentState().count;
35 | },
36 | },
37 | },
38 | },
39 | }),
40 | });
41 |
42 | const useCount = () => {
43 | const { loading, error, data } = useQuery(COUNT_QUERY);
44 | return (!loading && !error && data) ? selectCount(data) : 0;
45 | };
46 |
47 | const useIncrement = () => {
48 | const increment = () => currentState(reducer(currentState(), incrementAction));
49 | return increment;
50 | };
51 |
52 | const useDouble = () => {
53 | const doDouble = () => currentState(reducer(currentState(), doubleAction));
54 | return doDouble;
55 | };
56 |
57 | const Root = ({ children }) => (
58 |
59 | {children}
60 |
61 | );
62 |
63 | export default createApp(useCount, useIncrement, useDouble, Root);
64 |
--------------------------------------------------------------------------------
/src/common.js:
--------------------------------------------------------------------------------
1 | import React, {
2 | useDeferredValue,
3 | useEffect,
4 | useState,
5 | useTransition,
6 | useRef,
7 | } from 'react';
8 |
9 | // block for about 20 ms
10 | const syncBlock = () => {
11 | const start = performance.now();
12 | while (performance.now() - start < 20) {
13 | // empty
14 | }
15 | };
16 |
17 | export const initialState = {
18 | count: 0,
19 | };
20 |
21 | export const reducer = (state = initialState, action = {}) => {
22 | switch (action.type) {
23 | case 'increment':
24 | return {
25 | ...state,
26 | count: state.count + 1,
27 | };
28 | case 'double':
29 | return {
30 | ...state,
31 | count: state.count * 2,
32 | };
33 | default:
34 | return state;
35 | }
36 | };
37 |
38 | export const selectCount = (state) => state.count;
39 | export const incrementAction = { type: 'increment' };
40 | export const doubleAction = { type: 'double' };
41 |
42 | export const NUM_CHILD_COMPONENTS = 50;
43 | const ids = [...Array(NUM_CHILD_COMPONENTS).keys()];
44 |
45 | // check if all child components show the same count
46 | // and if not, change the title
47 | export const useCheckTearing = () => {
48 | useEffect(() => {
49 | try {
50 | const counts = ids.map((i) => Number(
51 | document.querySelector(`.count:nth-of-type(${i + 1})`).innerHTML,
52 | ));
53 | counts.push(Number(document.getElementById('mainCount').innerHTML));
54 | if (!counts.every((c) => c === counts[0])) {
55 | console.error('count mismatch', counts);
56 | document.title += ' TEARED';
57 | }
58 | } catch (e) {
59 | // ignored
60 | }
61 | });
62 | };
63 |
64 | export const createApp = (
65 | useCount,
66 | useIncrement,
67 | useDouble,
68 | Root = React.Fragment,
69 | componentWrapper = React.memo,
70 | mainWrapper = (fn) => fn,
71 | ) => {
72 | const Counter = componentWrapper(() => {
73 | const count = useCount();
74 | syncBlock();
75 | return {count}
;
76 | });
77 |
78 | const DeferredCounter = componentWrapper(() => {
79 | const count = useDeferredValue(useCount());
80 | syncBlock();
81 | return {count}
;
82 | });
83 |
84 | const Main = mainWrapper(() => {
85 | const [isPending, startTransition] = useTransition();
86 | const [mode, setMode] = useState(null);
87 | const transitionHide = () => {
88 | startTransition(() => setMode(null));
89 | };
90 | const transitionShowCounter = () => {
91 | startTransition(() => setMode('counter'));
92 | };
93 | const transitionShowDeferred = () => {
94 | startTransition(() => setMode('deferred'));
95 | };
96 | const count = useCount();
97 | const deferredCount = useDeferredValue(count);
98 | useCheckTearing();
99 | const increment = useIncrement();
100 | const doDouble = useDouble();
101 | const transitionIncrement = () => {
102 | startTransition(increment);
103 | };
104 | const timer = useRef();
105 | const stopAutoIncrement = () => {
106 | clearInterval(timer.current);
107 | };
108 | const startAutoIncrement = () => {
109 | stopAutoIncrement();
110 | timer.current = setInterval(increment, 50);
111 | };
112 | return (
113 |
114 |
117 |
120 |
123 |
126 |
129 |
132 |
135 |
138 |
{isPending && 'Pending...'}
139 |
Counters
140 | {mode === 'counter' && ids.map((id) =>
)}
141 | {mode === 'deferred' && ids.map((id) =>
)}
142 |
Main
143 |
{mode === 'deferred' ? deferredCount : count}
144 |
145 | );
146 | });
147 |
148 | const App = () => (
149 |
150 |
151 |
152 | );
153 |
154 | return App;
155 | };
156 |
--------------------------------------------------------------------------------
/src/constate/index.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import createUseContext from 'constate';
3 |
4 | import {
5 | reducer,
6 | initialState,
7 | selectCount,
8 | incrementAction,
9 | doubleAction,
10 | createApp,
11 | } from '../common';
12 |
13 | const useValue = () => React.useReducer(reducer, initialState);
14 | const [Root, useValueContext] = createUseContext(useValue);
15 |
16 | const useCount = () => {
17 | const [state] = useValueContext();
18 | return selectCount(state);
19 | };
20 |
21 | const useIncrement = () => {
22 | const [, dispatch] = useValueContext();
23 | return useCallback(() => dispatch(incrementAction), [dispatch]);
24 | };
25 |
26 | const useDouble = () => {
27 | const [, dispatch] = useValueContext();
28 | return useCallback(() => dispatch(doubleAction), [dispatch]);
29 | };
30 |
31 | export default createApp(useCount, useIncrement, useDouble, Root);
32 |
--------------------------------------------------------------------------------
/src/effector/index.js:
--------------------------------------------------------------------------------
1 | import { createStore, createEvent } from 'effector';
2 | import { useStore } from 'effector-react';
3 |
4 | import {
5 | reducer,
6 | initialState,
7 | selectCount,
8 | incrementAction,
9 | doubleAction,
10 | createApp,
11 | } from '../common';
12 |
13 | const dispatch = createEvent();
14 | const $store = createStore(initialState)
15 | .on(dispatch, reducer);
16 |
17 | const $count = $store.map((value) => selectCount(value));
18 |
19 | const useCount = () => useStore($count);
20 |
21 | const useIncrement = () => () => dispatch(incrementAction);
22 |
23 | const useDouble = () => () => dispatch(doubleAction);
24 |
25 | export default createApp(useCount, useIncrement, useDouble);
26 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 |
4 | const name = process.env.NAME || window.location.pathname.slice(1) || 'react-redux';
5 |
6 | // eslint-disable-next-line import/no-dynamic-require
7 | const App = require(`./${name}`).default;
8 |
9 | document.title = name;
10 |
11 | // concurrent mode
12 | const root = createRoot(document.getElementById('app'));
13 | root.render();
14 |
15 | // sync mode
16 | // ReactDOM.render(, document.getElementById('app'));
17 |
--------------------------------------------------------------------------------
/src/jotai/index.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import {
3 | Provider,
4 | atom,
5 | useAtom,
6 | useSetAtom,
7 | } from 'jotai';
8 |
9 | import {
10 | reducer,
11 | initialState,
12 | selectCount,
13 | incrementAction,
14 | doubleAction,
15 | createApp,
16 | } from '../common';
17 |
18 | const globalState = atom(initialState);
19 |
20 | const countState = atom(
21 | (get) => selectCount(get(globalState)),
22 | (get, set, action) => {
23 | set(globalState, reducer(get(globalState), action));
24 | },
25 | );
26 |
27 | const useCount = () => {
28 | const [count] = useAtom(countState);
29 | return count;
30 | };
31 |
32 | const useIncrement = () => {
33 | const dispatch = useSetAtom(countState);
34 | return useCallback(() => {
35 | dispatch(incrementAction);
36 | }, [dispatch]);
37 | };
38 |
39 | const useDouble = () => {
40 | const dispatch = useSetAtom(countState);
41 | return useCallback(() => {
42 | dispatch(doubleAction);
43 | }, [dispatch]);
44 | };
45 |
46 | const Root = ({ children }) => (
47 |
48 | {children}
49 |
50 | );
51 |
52 | export default createApp(useCount, useIncrement, useDouble, Root);
53 |
--------------------------------------------------------------------------------
/src/mobx-react-lite/index.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import { observable, runInAction } from 'mobx';
3 | import { observer } from 'mobx-react-lite';
4 |
5 | import {
6 | reducer,
7 | initialState,
8 | selectCount,
9 | incrementAction,
10 | doubleAction,
11 | createApp,
12 | } from '../common';
13 |
14 | const state = observable(initialState);
15 |
16 | const useCount = () => selectCount(state);
17 |
18 | const useIncrement = () => useCallback(() => {
19 | const newState = reducer(state, incrementAction);
20 | runInAction(() => {
21 | Object.keys(newState).forEach((key) => {
22 | state[key] = newState[key];
23 | });
24 | });
25 | }, []);
26 |
27 | const useDouble = () => useCallback(() => {
28 | const newState = reducer(state, doubleAction);
29 | runInAction(() => {
30 | Object.keys(newState).forEach((key) => {
31 | state[key] = newState[key];
32 | });
33 | });
34 | }, []);
35 |
36 | export default createApp(useCount, useIncrement, useDouble, React.Fragment, observer, observer);
37 |
--------------------------------------------------------------------------------
/src/react-hooks-global-state/index.js:
--------------------------------------------------------------------------------
1 | import { createStore } from 'react-hooks-global-state';
2 |
3 | import {
4 | reducer,
5 | initialState,
6 | incrementAction,
7 | doubleAction,
8 | createApp,
9 | } from '../common';
10 |
11 | const { dispatch, useGlobalState } = createStore(reducer, initialState);
12 |
13 | const useCount = () => {
14 | const [count] = useGlobalState('count');
15 | return count;
16 | };
17 |
18 | const useIncrement = () => () => dispatch(incrementAction);
19 |
20 | const useDouble = () => () => dispatch(doubleAction);
21 |
22 | export default createApp(useCount, useIncrement, useDouble);
23 |
--------------------------------------------------------------------------------
/src/react-query/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | QueryClient,
4 | QueryClientProvider,
5 | useQuery, useQueryClient,
6 | } from 'react-query';
7 |
8 | import {
9 | reducer,
10 | initialState,
11 | selectCount,
12 | incrementAction,
13 | doubleAction,
14 | createApp,
15 | } from '../common';
16 |
17 | const queryKey = ['counter'];
18 |
19 | const client = new QueryClient();
20 |
21 | const useCount = () => {
22 | const { data } = useQuery(
23 | queryKey,
24 | () => {
25 | throw new Error('should never be called');
26 | },
27 | {
28 | staleTime: Infinity,
29 | cacheTime: Infinity,
30 | initialData: initialState,
31 | },
32 | );
33 | return selectCount(data);
34 | };
35 |
36 | const useIncrement = () => {
37 | const queryClient = useQueryClient();
38 | const increment = () => queryClient.setQueryData(
39 | queryKey,
40 | (prev) => reducer(prev, incrementAction),
41 | );
42 | return increment;
43 | };
44 |
45 | const useDouble = () => {
46 | const queryClient = useQueryClient();
47 | const doDouble = () => queryClient.setQueryData(
48 | queryKey,
49 | (prev) => reducer(prev, doubleAction),
50 | );
51 | return doDouble;
52 | };
53 |
54 | const Root = ({ children }) => (
55 |
56 | {children}
57 |
58 | );
59 |
60 | export default createApp(useCount, useIncrement, useDouble, Root);
61 |
--------------------------------------------------------------------------------
/src/react-redux/index.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import { createStore } from 'redux';
3 | import { Provider, useSelector, useDispatch } from 'react-redux';
4 |
5 | import {
6 | reducer,
7 | selectCount,
8 | incrementAction,
9 | doubleAction,
10 | createApp,
11 | } from '../common';
12 |
13 | const store = createStore(reducer);
14 |
15 | const useCount = () => useSelector(selectCount);
16 |
17 | const useIncrement = () => {
18 | const dispatch = useDispatch();
19 | return useCallback(() => dispatch(incrementAction), [dispatch]);
20 | };
21 |
22 | const useDouble = () => {
23 | const dispatch = useDispatch();
24 | return useCallback(() => dispatch(doubleAction), [dispatch]);
25 | };
26 |
27 | const Root = ({ children }) => {children};
28 |
29 | export default createApp(useCount, useIncrement, useDouble, Root);
30 |
--------------------------------------------------------------------------------
/src/react-rxjs/index.js:
--------------------------------------------------------------------------------
1 | import { Subject, asapScheduler } from 'rxjs';
2 | import { map, scan, observeOn } from 'rxjs/operators';
3 | import { bind } from '@react-rxjs/core';
4 |
5 | import {
6 | reducer,
7 | initialState,
8 | selectCount,
9 | incrementAction,
10 | doubleAction,
11 | createApp,
12 | } from '../common';
13 |
14 | const actions$ = new Subject();
15 |
16 | const [useCount] = bind(
17 | actions$.pipe(
18 | observeOn(asapScheduler),
19 | scan(reducer, initialState),
20 | map(selectCount),
21 | ),
22 | selectCount(initialState),
23 | );
24 |
25 | const useIncrement = () => () => actions$.next(incrementAction);
26 | const useDouble = () => () => actions$.next(doubleAction);
27 |
28 | export default createApp(useCount, useIncrement, useDouble);
29 |
--------------------------------------------------------------------------------
/src/react-state/index.js:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useCallback } from 'react';
2 |
3 | import {
4 | reducer,
5 | initialState,
6 | selectCount,
7 | incrementAction,
8 | doubleAction,
9 | createApp,
10 | } from '../common';
11 |
12 | const StateCtx = createContext();
13 | const DispatchCtx = createContext();
14 |
15 | const useCount = () => selectCount(useContext(StateCtx));
16 |
17 | const useIncrement = () => {
18 | const dispatch = useContext(DispatchCtx);
19 | return useCallback(() => dispatch(incrementAction), [dispatch]);
20 | };
21 |
22 | const useDouble = () => {
23 | const dispatch = useContext(DispatchCtx);
24 | return useCallback(() => dispatch(doubleAction), [dispatch]);
25 | };
26 |
27 | const Root = ({ children }) => {
28 | const [state, dispatch] = React.useReducer(reducer, initialState);
29 | return (
30 |
31 |
32 | {children}
33 |
34 |
35 | );
36 | };
37 |
38 | export default createApp(useCount, useIncrement, useDouble, Root);
39 |
--------------------------------------------------------------------------------
/src/react-tracked/index.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import { createContainer } from 'react-tracked';
3 |
4 | import {
5 | reducer,
6 | initialState,
7 | selectCount,
8 | incrementAction,
9 | doubleAction,
10 | createApp,
11 | } from '../common';
12 |
13 | const useValue = () => React.useReducer(reducer, initialState);
14 |
15 | const {
16 | Provider: Root,
17 | useSelector,
18 | useUpdate: useDispatch,
19 | } = createContainer(useValue, true);
20 |
21 | const useCount = () => useSelector(selectCount);
22 |
23 | const useIncrement = () => {
24 | const dispatch = useDispatch();
25 | return useCallback(() => dispatch(incrementAction), [dispatch]);
26 | };
27 |
28 | const useDouble = () => {
29 | const dispatch = useDispatch();
30 | return useCallback(() => dispatch(doubleAction), [dispatch]);
31 | };
32 |
33 | export default createApp(useCount, useIncrement, useDouble, Root);
34 |
--------------------------------------------------------------------------------
/src/recoil/index.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import {
3 | RecoilRoot,
4 | useRecoilState,
5 | useSetRecoilState,
6 | atom,
7 | selector,
8 | } from 'recoil';
9 |
10 | import {
11 | reducer,
12 | initialState,
13 | selectCount,
14 | incrementAction,
15 | doubleAction,
16 | createApp,
17 | } from '../common';
18 |
19 | const globalState = atom({
20 | key: 'globalState',
21 | default: initialState,
22 | });
23 |
24 | const countState = selector({
25 | key: 'countState',
26 | get: ({ get }) => selectCount(get(globalState)),
27 | set: ({ get, set }, action) => {
28 | set(globalState, reducer(get(globalState), action));
29 | },
30 | });
31 |
32 | const useCount = () => {
33 | const [count] = useRecoilState(countState);
34 | return count;
35 | };
36 |
37 | const useIncrement = () => {
38 | const dispatch = useSetRecoilState(countState);
39 | return useCallback(() => {
40 | dispatch(incrementAction);
41 | }, [dispatch]);
42 | };
43 |
44 | const useDouble = () => {
45 | const dispatch = useSetRecoilState(countState);
46 | return useCallback(() => {
47 | dispatch(doubleAction);
48 | }, [dispatch]);
49 | };
50 |
51 | const Root = ({ children }) => (
52 |
53 | {children}
54 |
55 | );
56 |
57 | export default createApp(useCount, useIncrement, useDouble, Root);
58 |
--------------------------------------------------------------------------------
/src/recoil_UNSTABLE/index.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import {
3 | RecoilRoot,
4 | useRecoilState_TRANSITION_SUPPORT_UNSTABLE as useRecoilState,
5 | useSetRecoilState,
6 | atom,
7 | selector,
8 | } from 'recoil';
9 |
10 | import {
11 | reducer,
12 | initialState,
13 | selectCount,
14 | incrementAction,
15 | doubleAction,
16 | createApp,
17 | } from '../common';
18 |
19 | const globalState = atom({
20 | key: 'globalState',
21 | default: initialState,
22 | });
23 |
24 | const countState = selector({
25 | key: 'countState',
26 | get: ({ get }) => selectCount(get(globalState)),
27 | set: ({ get, set }, action) => {
28 | set(globalState, reducer(get(globalState), action));
29 | },
30 | });
31 |
32 | const useCount = () => {
33 | const [count] = useRecoilState(countState);
34 | return count;
35 | };
36 |
37 | const useIncrement = () => {
38 | const dispatch = useSetRecoilState(countState);
39 | return useCallback(() => {
40 | dispatch(incrementAction);
41 | }, [dispatch]);
42 | };
43 |
44 | const useDouble = () => {
45 | const dispatch = useSetRecoilState(countState);
46 | return useCallback(() => {
47 | dispatch(doubleAction);
48 | }, [dispatch]);
49 | };
50 |
51 | const Root = ({ children }) => (
52 |
53 | {children}
54 |
55 | );
56 |
57 | export default createApp(useCount, useIncrement, useDouble, Root);
58 |
--------------------------------------------------------------------------------
/src/simplux/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createSimpluxModule, createMutations, createSelectors } from '@simplux/core';
3 | import { SimpluxProvider, useSimplux } from '@simplux/react';
4 |
5 | import {
6 | initialState,
7 | selectCount,
8 | createApp,
9 | } from '../common';
10 |
11 | const counterModule = createSimpluxModule({
12 | name: 'counter',
13 | initialState,
14 | });
15 |
16 | const counter = {
17 | ...counterModule,
18 | ...createMutations(counterModule, {
19 | increment(state) {
20 | state.count += 1;
21 | },
22 | double(state) {
23 | state.count *= 2;
24 | },
25 | }),
26 | ...createSelectors(counterModule, {
27 | value: (state) => selectCount(state),
28 | }),
29 | };
30 |
31 | const useCount = () => useSimplux(counter.value);
32 |
33 | const useIncrement = () => () => counter.increment();
34 |
35 | const useDouble = () => () => counter.double();
36 |
37 | const Root = ({ children }) => (
38 |
39 | {children}
40 |
41 | );
42 |
43 | export default createApp(useCount, useIncrement, useDouble, Root);
44 |
--------------------------------------------------------------------------------
/src/use-atom/index.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import {
3 | Provider,
4 | atom,
5 | useAtom,
6 | useSetAtom,
7 | } from 'use-atom';
8 |
9 | import {
10 | reducer,
11 | initialState,
12 | selectCount,
13 | incrementAction,
14 | doubleAction,
15 | createApp,
16 | } from '../common';
17 |
18 | const globalState = atom(initialState);
19 |
20 | const countState = atom(
21 | (get) => selectCount(get(globalState)),
22 | (get, set, action) => {
23 | set(globalState, reducer(get(globalState), action));
24 | },
25 | );
26 |
27 | const useCount = () => {
28 | const [count] = useAtom(countState);
29 | return count;
30 | };
31 |
32 | const useIncrement = () => {
33 | const dispatch = useSetAtom(countState);
34 | return useCallback(() => {
35 | dispatch(incrementAction);
36 | }, [dispatch]);
37 | };
38 |
39 | const useDouble = () => {
40 | const dispatch = useSetAtom(countState);
41 | return useCallback(() => {
42 | dispatch(doubleAction);
43 | }, [dispatch]);
44 | };
45 |
46 | const Root = ({ children }) => (
47 |
48 | {children}
49 |
50 | );
51 |
52 | export default createApp(useCount, useIncrement, useDouble, Root);
53 |
--------------------------------------------------------------------------------
/src/use-context-selector-base/index.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useReducer } from 'react';
2 | import {
3 | createContext,
4 | useContextSelector,
5 | } from 'use-context-selector';
6 |
7 | import {
8 | reducer,
9 | initialState,
10 | selectCount,
11 | incrementAction,
12 | doubleAction,
13 | createApp,
14 | } from '../common';
15 |
16 | const context = createContext(null);
17 |
18 | const useCount = () => useContextSelector(context, (v) => selectCount(v[0]));
19 |
20 | const useIncrement = () => {
21 | const dispatch = useContextSelector(context, (v) => v[1]);
22 | return useCallback(
23 | () => dispatch(incrementAction),
24 | [dispatch],
25 | );
26 | };
27 |
28 | const useDouble = () => {
29 | const dispatch = useContextSelector(context, (v) => v[1]);
30 | return useCallback(
31 | () => dispatch(doubleAction),
32 | [dispatch],
33 | );
34 | };
35 |
36 | const Root = ({ children }) => (
37 |
38 | {children}
39 |
40 | );
41 |
42 | export default createApp(useCount, useIncrement, useDouble, Root);
43 |
--------------------------------------------------------------------------------
/src/use-context-selector/index.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useReducer } from 'react';
2 | import {
3 | createContext,
4 | useContextSelector,
5 | useContextUpdate,
6 | } from 'use-context-selector';
7 |
8 | import {
9 | reducer,
10 | initialState,
11 | selectCount,
12 | incrementAction,
13 | doubleAction,
14 | createApp,
15 | } from '../common';
16 |
17 | const context = createContext(null);
18 |
19 | const useCount = () => useContextSelector(context, (v) => selectCount(v[0]));
20 |
21 | const useIncrement = () => {
22 | const update = useContextUpdate(context);
23 | const dispatch = useContextSelector(context, (v) => v[1]);
24 | return useCallback(
25 | () => update(() => dispatch(incrementAction)),
26 | [update, dispatch],
27 | );
28 | };
29 |
30 | const useDouble = () => {
31 | const update = useContextUpdate(context);
32 | const dispatch = useContextSelector(context, (v) => v[1]);
33 | return useCallback(
34 | () => update(() => dispatch(doubleAction)),
35 | [update, dispatch],
36 | );
37 | };
38 |
39 | const Root = ({ children }) => (
40 |
41 | {children}
42 |
43 | );
44 |
45 | export default createApp(useCount, useIncrement, useDouble, Root);
46 |
--------------------------------------------------------------------------------
/src/use-subscription/index.js:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import { createStore } from 'redux';
3 | import { useSubscription } from 'use-subscription';
4 |
5 | import {
6 | reducer,
7 | selectCount,
8 | incrementAction,
9 | doubleAction,
10 | createApp,
11 | } from '../common';
12 |
13 | const store = createStore(reducer);
14 |
15 | const useCount = () => {
16 | const count = useSubscription(useMemo(() => ({
17 | getCurrentValue: () => selectCount(store.getState()),
18 | subscribe: (callback) => store.subscribe(callback),
19 | }), []));
20 | return count;
21 | };
22 |
23 | const useIncrement = () => () => store.dispatch(incrementAction);
24 |
25 | const useDouble = () => () => store.dispatch(doubleAction);
26 |
27 | export default createApp(useCount, useIncrement, useDouble);
28 |
--------------------------------------------------------------------------------
/src/valtio/index.js:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react';
2 | import { proxy, useSnapshot } from 'valtio';
3 |
4 | import {
5 | reducer,
6 | initialState,
7 | selectCount,
8 | incrementAction,
9 | doubleAction,
10 | createApp,
11 | } from '../common';
12 |
13 | const state = proxy(initialState);
14 |
15 | const useCount = () => selectCount(useSnapshot(state, { sync: true }));
16 |
17 | const useIncrement = () => useCallback(() => {
18 | const newState = reducer(state, incrementAction);
19 | Object.keys(newState).forEach((key) => {
20 | state[key] = newState[key];
21 | });
22 | }, []);
23 |
24 | const useDouble = () => useCallback(() => {
25 | const newState = reducer(state, doubleAction);
26 | Object.keys(newState).forEach((key) => {
27 | state[key] = newState[key];
28 | });
29 | }, []);
30 |
31 | export default createApp(useCount, useIncrement, useDouble);
32 |
--------------------------------------------------------------------------------
/src/zustand/index.js:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react';
2 | import create from 'zustand';
3 |
4 | import {
5 | reducer,
6 | initialState,
7 | selectCount,
8 | incrementAction,
9 | doubleAction,
10 | createApp,
11 | } from '../common';
12 |
13 | const useStore = create((set) => ({
14 | ...initialState,
15 | dispatch: (action) => set((state) => reducer(state, action)),
16 | }));
17 |
18 | const useCount = () => useStore(selectCount);
19 |
20 | const selectDispatch = (state) => state.dispatch;
21 |
22 | const useIncrement = () => {
23 | const dispatch = useStore(selectDispatch);
24 | return useCallback(() => dispatch(incrementAction), [dispatch]);
25 | };
26 |
27 | const useDouble = () => {
28 | const dispatch = useStore(selectDispatch);
29 | return useCallback(() => dispatch(doubleAction), [dispatch]);
30 | };
31 |
32 | export default createApp(useCount, useIncrement, useDouble);
33 |
--------------------------------------------------------------------------------
/update_readme.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 |
3 | const libraries = {
4 | 'react-redux': 'react-redux',
5 | zustand: 'zustand',
6 | 'react-tracked': 'react-tracked',
7 | constate: 'constate',
8 | 'react-hooks-global-state': 'react-hooks-global-state',
9 | 'use-context-selector-base': 'use-context-selector (w/ useReducer, w/o useContextUpdate)',
10 | 'use-context-selector': 'use-context-selector (w/ useReducer)',
11 | 'use-subscription': 'use-subscription (w/ redux)',
12 | 'apollo-client': 'apollo-client',
13 | recoil: 'recoil',
14 | recoil_UNSTABLE: 'recoil (UNSTABLE)',
15 | jotai: 'jotai',
16 | 'use-atom': 'use-atom',
17 | valtio: 'valtio',
18 | effector: 'effector',
19 | 'react-rxjs': 'react-rxjs',
20 | simplux: 'simplux',
21 | 'react-query': 'react-query',
22 | 'mobx-react-lite': 'mobx-react-lite',
23 | };
24 |
25 | const numTests = 10;
26 |
27 | function wrap(content, tag) { return `<${tag}>${content}${tag}>`; }
28 | function check(status) { return status === 'failed' ? ':x:' : ':white_check_mark:'; }
29 |
30 | // Get results into an array of test with a 2nd dimension by test/fail
31 | const results = JSON.parse(fs.readFileSync('./outfile.json', 'utf8'));
32 | const testResults = [];
33 | results.testResults[0].assertionResults.forEach((result, ix) => {
34 | const testNumber = Math.floor(ix / numTests);
35 | testResults[testNumber] = testResults[testNumber] || [];
36 | testResults[testNumber][ix % numTests] = {
37 | status: result.status,
38 | title: result.ancestorTitles[0],
39 | };
40 | });
41 |
42 | // Format table for substitution in outfile
43 | let sub = '';
44 | testResults.forEach((result) => {
45 | if (!libraries[result[0].title]) {
46 | console.info('no library entry for', result[0].title);
47 | return;
48 | }
49 | const th = wrap(libraries[result[0].title], 'th');
50 | const tds = result.map((test) => `\t\t${wrap(check(test.status), 'td')}\n`).join('');
51 | sub += `\t\n\t\t${th}\n${tds}\t
\n`;
52 | });
53 |
54 | // Find first and last line of raw results
55 | let first = 0;
56 | let last = 0;
57 | function note(line, ix) {
58 | if (line.match(/[✓✕]/)) {
59 | if (!first) first = ix - 6;
60 | last = ix;
61 | }
62 | // eslint-disable-next-line no-control-regex
63 | return line.replace(/..\r/g, '').replace(/\[.../g, '').substr(5);
64 | }
65 |
66 | // Read and process raw results
67 | const resultsRaw = fs.readFileSync('./outfile_raw.txt', 'utf8');
68 | let lines = resultsRaw.split(/\n\r|\n/).map((l, i) => note(l.substr(0), i));
69 | lines = lines.slice(first, last + 1);
70 | lines = lines.filter((line, ix) => ix % 3 === 0);
71 |
72 | // Update readme
73 | let readme = fs.readFileSync('./README.md', 'utf8');
74 | readme = readme.replace(
75 | /([\s\S]*?)<\/table>/,
76 | `\nTest | ${Array.from(Array(numTests).keys()).map((i) => `${i + 1} | `).join('')}
\n${sub}\n
`,
77 | );
78 | readme = readme.replace(
79 | /([\s\S]*?)<\/details>/,
80 | `\nRaw Output
\n\n\`\`\`\n${lines.join('\n')}\n\n\`\`\`\n `,
81 | );
82 |
83 | fs.writeFileSync('./README.md', readme);
84 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 |
5 | module.exports = {
6 | mode: 'production',
7 | entry: './src/index.js',
8 | output: {
9 | path: path.join(__dirname, 'dist', process.env.NAME || 'react-redux'),
10 | filename: '[name].js',
11 | },
12 | plugins: [
13 | new webpack.DefinePlugin({
14 | __DEV__: JSON.stringify(process.env.NODE_ENV !== 'production'),
15 | }),
16 | new webpack.EnvironmentPlugin({
17 | NAME: '',
18 | }),
19 | new HtmlWebpackPlugin({
20 | template: './public/index.html',
21 | }),
22 | ],
23 | module: {
24 | rules: [{
25 | test: /\.jsx?$/,
26 | resolve: {
27 | fullySpecified: false,
28 | },
29 | exclude: /node_modules\/(?!(@apollo)\/).*/,
30 | use: [{
31 | loader: 'babel-loader',
32 | options: {
33 | presets: [
34 | ['@babel/preset-env', { modules: false }],
35 | '@babel/preset-react',
36 | ],
37 | },
38 | }],
39 | }],
40 | },
41 | devServer: {
42 | port: process.env.PORT || '8080',
43 | historyApiFallback: true,
44 | },
45 | };
46 |
--------------------------------------------------------------------------------