├── .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
├── 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 intentionaly 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 (8111 ms)
95 | ✓ No tearing finally on mount (4750 ms)
96 | Level 2
97 | ✓ No tearing temporarily on update (13089 ms)
98 | ✓ No tearing temporarily on mount (4658 ms)
99 | Level 3
100 | ✕ Can interrupt render (time slicing) (8052 ms)
101 | ✕ Can branch state (wip state) (6770 ms)
102 | With useDeferredValue
103 | Level 1
104 | ✓ No tearing finally on update (9729 ms)
105 | ✓ No tearing finally on mount (4734 ms)
106 | Level 2
107 | ✓ No tearing temporarily on update (14749 ms)
108 | ✓ No tearing temporarily on mount (4728 ms)
109 | zustand
110 | With useTransition
111 | Level 1
112 | ✓ No tearing finally on update (8125 ms)
113 | ✓ No tearing finally on mount (4687 ms)
114 | Level 2
115 | ✓ No tearing temporarily on update (13062 ms)
116 | ✓ No tearing temporarily on mount (4666 ms)
117 | Level 3
118 | ✕ Can interrupt render (time slicing) (8024 ms)
119 | ✕ Can branch state (wip state) (6772 ms)
120 | With useDeferredValue
121 | Level 1
122 | ✓ No tearing finally on update (9725 ms)
123 | ✓ No tearing finally on mount (4692 ms)
124 | Level 2
125 | ✓ No tearing temporarily on update (14756 ms)
126 | ✓ No tearing temporarily on mount (4670 ms)
127 | react-tracked
128 | With useTransition
129 | Level 1
130 | ✓ No tearing finally on update (5632 ms)
131 | ✓ No tearing finally on mount (9615 ms)
132 | Level 2
133 | ✓ No tearing temporarily on update (8722 ms)
134 | ✓ No tearing temporarily on mount (9589 ms)
135 | Level 3
136 | ✓ Can interrupt render (time slicing) (3669 ms)
137 | ✓ Can branch state (wip state) (8249 ms)
138 | With useDeferredValue
139 | Level 1
140 | ✓ No tearing finally on update (15459 ms)
141 | ✓ No tearing finally on mount (6622 ms)
142 | Level 2
143 | ✓ No tearing temporarily on update (19592 ms)
144 | ✓ No tearing temporarily on mount (6537 ms)
145 | constate
146 | With useTransition
147 | Level 1
148 | ✓ No tearing finally on update (4647 ms)
149 | ✓ No tearing finally on mount (7621 ms)
150 | Level 2
151 | ✓ No tearing temporarily on update (8764 ms)
152 | ✓ No tearing temporarily on mount (7591 ms)
153 | Level 3
154 | ✓ Can interrupt render (time slicing) (3714 ms)
155 | ✓ Can branch state (wip state) (5250 ms)
156 | With useDeferredValue
157 | Level 1
158 | ✓ No tearing finally on update (9732 ms)
159 | ✓ No tearing finally on mount (5751 ms)
160 | Level 2
161 | ✓ No tearing temporarily on update (14753 ms)
162 | ✓ No tearing temporarily on mount (5707 ms)
163 | react-hooks-global-state
164 | With useTransition
165 | Level 1
166 | ✓ No tearing finally on update (8100 ms)
167 | ✓ No tearing finally on mount (4751 ms)
168 | Level 2
169 | ✓ No tearing temporarily on update (13116 ms)
170 | ✓ No tearing temporarily on mount (4700 ms)
171 | Level 3
172 | ✕ Can interrupt render (time slicing) (8062 ms)
173 | ✕ Can branch state (wip state) (6786 ms)
174 | With useDeferredValue
175 | Level 1
176 | ✓ No tearing finally on update (9723 ms)
177 | ✓ No tearing finally on mount (4731 ms)
178 | Level 2
179 | ✓ No tearing temporarily on update (14743 ms)
180 | ✓ No tearing temporarily on mount (4708 ms)
181 | use-context-selector-base
182 | With useTransition
183 | Level 1
184 | ✓ No tearing finally on update (8020 ms)
185 | ✓ No tearing finally on mount (7604 ms)
186 | Level 2
187 | ✓ No tearing temporarily on update (13002 ms)
188 | ✓ No tearing temporarily on mount (8606 ms)
189 | Level 3
190 | ✕ Can interrupt render (time slicing) (7974 ms)
191 | ✕ Can branch state (wip state) (7783 ms)
192 | With useDeferredValue
193 | Level 1
194 | ✓ No tearing finally on update (9726 ms)
195 | ✓ No tearing finally on mount (5784 ms)
196 | Level 2
197 | ✓ No tearing temporarily on update (14788 ms)
198 | ✓ No tearing temporarily on mount (5710 ms)
199 | use-context-selector
200 | With useTransition
201 | Level 1
202 | ✓ No tearing finally on update (5554 ms)
203 | ✓ No tearing finally on mount (11611 ms)
204 | Level 2
205 | ✓ No tearing temporarily on update (8704 ms)
206 | ✓ No tearing temporarily on mount (11607 ms)
207 | Level 3
208 | ✓ Can interrupt render (time slicing) (3672 ms)
209 | ✓ Can branch state (wip state) (8246 ms)
210 | With useDeferredValue
211 | Level 1
212 | ✓ No tearing finally on update (15470 ms)
213 | ✓ No tearing finally on mount (8634 ms)
214 | Level 2
215 | ✓ No tearing temporarily on update (19581 ms)
216 | ✓ No tearing temporarily on mount (6546 ms)
217 | use-subscription
218 | With useTransition
219 | Level 1
220 | ✓ No tearing finally on update (8099 ms)
221 | ✓ No tearing finally on mount (4675 ms)
222 | Level 2
223 | ✓ No tearing temporarily on update (13124 ms)
224 | ✓ No tearing temporarily on mount (4722 ms)
225 | Level 3
226 | ✕ Can interrupt render (time slicing) (8035 ms)
227 | ✕ Can branch state (wip state) (6806 ms)
228 | With useDeferredValue
229 | Level 1
230 | ✓ No tearing finally on update (9767 ms)
231 | ✓ No tearing finally on mount (4717 ms)
232 | Level 2
233 | ✓ No tearing temporarily on update (14739 ms)
234 | ✓ No tearing temporarily on mount (4732 ms)
235 | apollo-client
236 | With useTransition
237 | Level 1
238 | ✓ No tearing finally on update (8281 ms)
239 | ✓ No tearing finally on mount (5745 ms)
240 | Level 2
241 | ✓ No tearing temporarily on update (13237 ms)
242 | ✓ No tearing temporarily on mount (5695 ms)
243 | Level 3
244 | ✕ Can interrupt render (time slicing) (8222 ms)
245 | ✕ Can branch state (wip state) (7864 ms)
246 | With useDeferredValue
247 | Level 1
248 | ✓ No tearing finally on update (6578 ms)
249 | ✓ No tearing finally on mount (5705 ms)
250 | Level 2
251 | ✓ No tearing temporarily on update (9707 ms)
252 | ✓ No tearing temporarily on mount (5712 ms)
253 | recoil
254 | With useTransition
255 | Level 1
256 | ✓ No tearing finally on update (8152 ms)
257 | ✓ No tearing finally on mount (4745 ms)
258 | Level 2
259 | ✓ No tearing temporarily on update (13157 ms)
260 | ✓ No tearing temporarily on mount (4749 ms)
261 | Level 3
262 | ✕ Can interrupt render (time slicing) (8120 ms)
263 | ✕ Can branch state (wip state) (6843 ms)
264 | With useDeferredValue
265 | Level 1
266 | ✓ No tearing finally on update (9760 ms)
267 | ✓ No tearing finally on mount (4721 ms)
268 | Level 2
269 | ✓ No tearing temporarily on update (14767 ms)
270 | ✓ No tearing temporarily on mount (4737 ms)
271 | recoil_UNSTABLE
272 | With useTransition
273 | Level 1
274 | ✓ No tearing finally on update (5645 ms)
275 | ✓ No tearing finally on mount (6650 ms)
276 | Level 2
277 | ✓ No tearing temporarily on update (8754 ms)
278 | ✕ No tearing temporarily on mount (5640 ms)
279 | Level 3
280 | ✓ Can interrupt render (time slicing) (3717 ms)
281 | ✕ Can branch state (wip state) (10319 ms)
282 | With useDeferredValue
283 | Level 1
284 | ✓ No tearing finally on update (11378 ms)
285 | ✓ No tearing finally on mount (5713 ms)
286 | Level 2
287 | ✓ No tearing temporarily on update (15525 ms)
288 | ✕ No tearing temporarily on mount (5665 ms)
289 | jotai
290 | With useTransition
291 | Level 1
292 | ✓ No tearing finally on update (5618 ms)
293 | ✓ No tearing finally on mount (8607 ms)
294 | Level 2
295 | ✓ No tearing temporarily on update (9730 ms)
296 | ✕ No tearing temporarily on mount (8578 ms)
297 | Level 3
298 | ✓ Can interrupt render (time slicing) (4676 ms)
299 | ✕ Can branch state (wip state) (10271 ms)
300 | With useDeferredValue
301 | Level 1
302 | ✓ No tearing finally on update (10739 ms)
303 | ✓ No tearing finally on mount (6706 ms)
304 | Level 2
305 | ✓ No tearing temporarily on update (15734 ms)
306 | ✕ No tearing temporarily on mount (5714 ms)
307 | use-atom
308 | With useTransition
309 | Level 1
310 | ✓ No tearing finally on update (6626 ms)
311 | ✓ No tearing finally on mount (11617 ms)
312 | Level 2
313 | ✓ No tearing temporarily on update (9775 ms)
314 | ✓ No tearing temporarily on mount (11589 ms)
315 | Level 3
316 | ✓ Can interrupt render (time slicing) (4690 ms)
317 | ✓ Can branch state (wip state) (9250 ms)
318 | With useDeferredValue
319 | Level 1
320 | ✓ No tearing finally on update (16475 ms)
321 | ✓ No tearing finally on mount (6627 ms)
322 | Level 2
323 | ✓ No tearing temporarily on update (20627 ms)
324 | ✓ No tearing temporarily on mount (6540 ms)
325 | valtio
326 | With useTransition
327 | Level 1
328 | ✓ No tearing finally on update (8110 ms)
329 | ✓ No tearing finally on mount (4768 ms)
330 | Level 2
331 | ✓ No tearing temporarily on update (13113 ms)
332 | ✓ No tearing temporarily on mount (4706 ms)
333 | Level 3
334 | ✕ Can interrupt render (time slicing) (8121 ms)
335 | ✕ Can branch state (wip state) (6826 ms)
336 | With useDeferredValue
337 | Level 1
338 | ✓ No tearing finally on update (9745 ms)
339 | ✓ No tearing finally on mount (4755 ms)
340 | Level 2
341 | ✓ No tearing temporarily on update (14761 ms)
342 | ✓ No tearing temporarily on mount (4708 ms)
343 | effector
344 | With useTransition
345 | Level 1
346 | ✓ No tearing finally on update (10346 ms)
347 | ✓ No tearing finally on mount (4740 ms)
348 | Level 2
349 | ✓ No tearing temporarily on update (13097 ms)
350 | ✓ No tearing temporarily on mount (4744 ms)
351 | Level 3
352 | ✕ Can interrupt render (time slicing) (8078 ms)
353 | ✕ Can branch state (wip state) (6775 ms)
354 | With useDeferredValue
355 | Level 1
356 | ✓ No tearing finally on update (9720 ms)
357 | ✓ No tearing finally on mount (4734 ms)
358 | Level 2
359 | ✓ No tearing temporarily on update (14758 ms)
360 | ✓ No tearing temporarily on mount (4709 ms)
361 | react-rxjs
362 | With useTransition
363 | Level 1
364 | ✓ No tearing finally on update (8130 ms)
365 | ✓ No tearing finally on mount (4737 ms)
366 | Level 2
367 | ✓ No tearing temporarily on update (13148 ms)
368 | ✓ No tearing temporarily on mount (4702 ms)
369 | Level 3
370 | ✕ Can interrupt render (time slicing) (8085 ms)
371 | ✕ Can branch state (wip state) (6770 ms)
372 | With useDeferredValue
373 | Level 1
374 | ✓ No tearing finally on update (9747 ms)
375 | ✓ No tearing finally on mount (4728 ms)
376 | Level 2
377 | ✓ No tearing temporarily on update (14747 ms)
378 | ✓ No tearing temporarily on mount (4677 ms)
379 | simplux
380 | With useTransition
381 | Level 1
382 | ✓ No tearing finally on update (4634 ms)
383 | ✓ No tearing finally on mount (8634 ms)
384 | Level 2
385 | ✓ No tearing temporarily on update (8760 ms)
386 | ✓ No tearing temporarily on mount (6573 ms)
387 | Level 3
388 | ✓ Can interrupt render (time slicing) (3705 ms)
389 | ✕ Can branch state (wip state) (9243 ms)
390 | With useDeferredValue
391 | Level 1
392 | ✓ No tearing finally on update (9706 ms)
393 | ✓ No tearing finally on mount (6720 ms)
394 | Level 2
395 | ✓ No tearing temporarily on update (14750 ms)
396 | ✓ No tearing temporarily on mount (6629 ms)
397 | react-query
398 | With useTransition
399 | Level 1
400 | ✓ No tearing finally on update (8195 ms)
401 | ✓ No tearing finally on mount (4714 ms)
402 | Level 2
403 | ✕ No tearing temporarily on update (13166 ms)
404 | ✓ No tearing temporarily on mount (4711 ms)
405 | Level 3
406 | ✕ Can interrupt render (time slicing) (8127 ms)
407 | ✕ Can branch state (wip state) (6812 ms)
408 | With useDeferredValue
409 | Level 1
410 | ✓ No tearing finally on update (9629 ms)
411 | ✓ No tearing finally on mount (4679 ms)
412 | Level 2
413 | ✓ No tearing temporarily on update (13776 ms)
414 | ✓ No tearing temporarily on mount (4714 ms)
415 |
416 | ```
417 |
418 |
419 |
420 | Test | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
421 |
422 | react-redux |
423 | :white_check_mark: |
424 | :white_check_mark: |
425 | :white_check_mark: |
426 | :white_check_mark: |
427 | :x: |
428 | :x: |
429 | :white_check_mark: |
430 | :white_check_mark: |
431 | :white_check_mark: |
432 | :white_check_mark: |
433 |
434 |
435 | zustand |
436 | :white_check_mark: |
437 | :white_check_mark: |
438 | :white_check_mark: |
439 | :white_check_mark: |
440 | :x: |
441 | :x: |
442 | :white_check_mark: |
443 | :white_check_mark: |
444 | :white_check_mark: |
445 | :white_check_mark: |
446 |
447 |
448 | react-tracked |
449 | :white_check_mark: |
450 | :white_check_mark: |
451 | :white_check_mark: |
452 | :white_check_mark: |
453 | :white_check_mark: |
454 | :white_check_mark: |
455 | :white_check_mark: |
456 | :white_check_mark: |
457 | :white_check_mark: |
458 | :white_check_mark: |
459 |
460 |
461 | constate |
462 | :white_check_mark: |
463 | :white_check_mark: |
464 | :white_check_mark: |
465 | :white_check_mark: |
466 | :white_check_mark: |
467 | :white_check_mark: |
468 | :white_check_mark: |
469 | :white_check_mark: |
470 | :white_check_mark: |
471 | :white_check_mark: |
472 |
473 |
474 | react-hooks-global-state |
475 | :white_check_mark: |
476 | :white_check_mark: |
477 | :white_check_mark: |
478 | :white_check_mark: |
479 | :x: |
480 | :x: |
481 | :white_check_mark: |
482 | :white_check_mark: |
483 | :white_check_mark: |
484 | :white_check_mark: |
485 |
486 |
487 | use-context-selector (w/ useReducer, w/o useContextUpdate) |
488 | :white_check_mark: |
489 | :white_check_mark: |
490 | :white_check_mark: |
491 | :white_check_mark: |
492 | :x: |
493 | :x: |
494 | :white_check_mark: |
495 | :white_check_mark: |
496 | :white_check_mark: |
497 | :white_check_mark: |
498 |
499 |
500 | use-context-selector (w/ useReducer) |
501 | :white_check_mark: |
502 | :white_check_mark: |
503 | :white_check_mark: |
504 | :white_check_mark: |
505 | :white_check_mark: |
506 | :white_check_mark: |
507 | :white_check_mark: |
508 | :white_check_mark: |
509 | :white_check_mark: |
510 | :white_check_mark: |
511 |
512 |
513 | use-subscription (w/ redux) |
514 | :white_check_mark: |
515 | :white_check_mark: |
516 | :white_check_mark: |
517 | :white_check_mark: |
518 | :x: |
519 | :x: |
520 | :white_check_mark: |
521 | :white_check_mark: |
522 | :white_check_mark: |
523 | :white_check_mark: |
524 |
525 |
526 | apollo-client |
527 | :white_check_mark: |
528 | :white_check_mark: |
529 | :white_check_mark: |
530 | :white_check_mark: |
531 | :x: |
532 | :x: |
533 | :white_check_mark: |
534 | :white_check_mark: |
535 | :white_check_mark: |
536 | :white_check_mark: |
537 |
538 |
539 | recoil |
540 | :white_check_mark: |
541 | :white_check_mark: |
542 | :white_check_mark: |
543 | :white_check_mark: |
544 | :x: |
545 | :x: |
546 | :white_check_mark: |
547 | :white_check_mark: |
548 | :white_check_mark: |
549 | :white_check_mark: |
550 |
551 |
552 | recoil (UNSTABLE) |
553 | :white_check_mark: |
554 | :white_check_mark: |
555 | :white_check_mark: |
556 | :x: |
557 | :white_check_mark: |
558 | :x: |
559 | :white_check_mark: |
560 | :white_check_mark: |
561 | :white_check_mark: |
562 | :x: |
563 |
564 |
565 | jotai |
566 | :white_check_mark: |
567 | :white_check_mark: |
568 | :white_check_mark: |
569 | :x: |
570 | :white_check_mark: |
571 | :x: |
572 | :white_check_mark: |
573 | :white_check_mark: |
574 | :white_check_mark: |
575 | :x: |
576 |
577 |
578 | use-atom |
579 | :white_check_mark: |
580 | :white_check_mark: |
581 | :white_check_mark: |
582 | :white_check_mark: |
583 | :white_check_mark: |
584 | :white_check_mark: |
585 | :white_check_mark: |
586 | :white_check_mark: |
587 | :white_check_mark: |
588 | :white_check_mark: |
589 |
590 |
591 | valtio |
592 | :white_check_mark: |
593 | :white_check_mark: |
594 | :white_check_mark: |
595 | :white_check_mark: |
596 | :x: |
597 | :x: |
598 | :white_check_mark: |
599 | :white_check_mark: |
600 | :white_check_mark: |
601 | :white_check_mark: |
602 |
603 |
604 | effector |
605 | :white_check_mark: |
606 | :white_check_mark: |
607 | :white_check_mark: |
608 | :white_check_mark: |
609 | :x: |
610 | :x: |
611 | :white_check_mark: |
612 | :white_check_mark: |
613 | :white_check_mark: |
614 | :white_check_mark: |
615 |
616 |
617 | react-rxjs |
618 | :white_check_mark: |
619 | :white_check_mark: |
620 | :white_check_mark: |
621 | :white_check_mark: |
622 | :x: |
623 | :x: |
624 | :white_check_mark: |
625 | :white_check_mark: |
626 | :white_check_mark: |
627 | :white_check_mark: |
628 |
629 |
630 | simplux |
631 | :white_check_mark: |
632 | :white_check_mark: |
633 | :white_check_mark: |
634 | :white_check_mark: |
635 | :white_check_mark: |
636 | :x: |
637 | :white_check_mark: |
638 | :white_check_mark: |
639 | :white_check_mark: |
640 | :white_check_mark: |
641 |
642 |
643 | react-query |
644 | :white_check_mark: |
645 | :white_check_mark: |
646 | :x: |
647 | :white_check_mark: |
648 | :x: |
649 | :x: |
650 | :white_check_mark: |
651 | :white_check_mark: |
652 | :white_check_mark: |
653 | :white_check_mark: |
654 |
655 |
656 |
657 |
658 | ## Caveats
659 |
660 | - Tearing and state branching may not be an issue depending on app requirements.
661 | - The test is done in a very limited way.
662 | - Passing tests don't guarantee anything.
663 | - The results may not be accurate.
664 | - Do not fully trust the results.
665 |
666 | ## If you are interested
667 |
668 | The reason why I created this is to test my projects.
669 |
670 | - [react-tracked](https://github.com/dai-shi/react-tracked)
671 | - [use-context-selector](https://github.com/dai-shi/use-context-selector)
672 | - and so on
673 |
674 | ## Contributing
675 |
676 | This repository is a tool for us to test some of global state libraries.
677 | While it is totally fine to use the tool for other libraries under the license,
678 | we don't generally accept adding a new library to the repository.
679 |
680 | However, we are interested in various approaches.
681 | If you have any suggestions feel free to open issues or pull requests.
682 | We may consider adding (and removing) libraries.
683 | Questions and discussions are also welcome in issues.
684 |
685 | For listing global state libraries, we have another repository
686 | https://github.com/dai-shi/lets-compare-global-state-with-react-hooks
687 | in which we accept contributions. It's recommended to run this tool
688 | and we put the result there, possibly a reference link to a PR
689 | in this repository or a fork of this repository.
690 |
--------------------------------------------------------------------------------
/__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 | ];
37 |
38 | names.forEach((name) => {
39 | describe(name, () => {
40 | beforeEach(async () => {
41 | await page.goto(`http://localhost:${port}/${name}/index.html`);
42 | await sleep(1000); // to make it stable
43 | });
44 |
45 | afterEach(async () => {
46 | await jestPuppeteer.resetBrowser();
47 | });
48 |
49 | describe('With useTransition', () => {
50 | describe('Level 1', () => {
51 | it('No tearing finally on update', async () => {
52 | await page.click('#transitionShowCounter');
53 | // wait until all counts become zero
54 | await Promise.all(ids.map(async (i) => {
55 | await expect(page).toMatchElement(`.count:nth-of-type(${i + 1})`, {
56 | text: '0',
57 | timeout: 5 * 1000,
58 | });
59 | }));
60 | for (let loop = 0; loop < REPEAT; loop += 1) {
61 | await page.click('#transitionIncrement');
62 | await sleep(100);
63 | }
64 | // check if all counts become REPEAT
65 | await Promise.all(ids.map(async (i) => {
66 | await expect(page).toMatchElement(`.count:nth-of-type(${i + 1})`, {
67 | text: `${REPEAT}`,
68 | timeout: 10 * 1000,
69 | });
70 | }));
71 | });
72 |
73 | it('No tearing finally on mount', async () => {
74 | await page.click('#startAutoIncrement');
75 | await sleep(100);
76 | await page.click('#transitionShowCounter');
77 | await sleep(1000);
78 | await page.click('#stopAutoIncrement');
79 | await sleep(2000);
80 | const count = page.evaluate(() => document.querySelector('.count:first-of-type'));
81 | await Promise.all(ids.map(async (i) => {
82 | await expect(page).toMatchElement(`.count:nth-of-type(${i + 1})`, {
83 | text: count,
84 | timeout: 10 * 1000,
85 | });
86 | }));
87 | });
88 | });
89 |
90 | describe('Level 2', () => {
91 | it('No tearing temporarily on update', async () => {
92 | await page.click('#transitionShowCounter');
93 | // wait until all counts become zero
94 | await Promise.all(ids.map(async (i) => {
95 | await expect(page).toMatchElement(`.count:nth-of-type(${i + 1})`, {
96 | text: '0',
97 | timeout: 5 * 1000,
98 | });
99 | }));
100 | for (let loop = 0; loop < REPEAT; loop += 1) {
101 | await page.click('#transitionIncrement');
102 | await sleep(100);
103 | }
104 | await sleep(5000);
105 | // check if there's inconsistency during update
106 | // see useCheckTearing() in src/common.js
107 | await expect(page.title()).resolves.not.toMatch(/TEARED/);
108 | });
109 |
110 | it('No tearing temporarily on mount', async () => {
111 | await page.click('#startAutoIncrement');
112 | await sleep(100);
113 | await page.click('#transitionShowCounter');
114 | await sleep(1000);
115 | await page.click('#stopAutoIncrement');
116 | await sleep(2000);
117 | // check if there's inconsistency during update
118 | // see useCheckTearing() in src/common.js
119 | await expect(page.title()).resolves.not.toMatch(/TEARED/);
120 | });
121 | });
122 |
123 | describe('Level 3', () => {
124 | it('Can interrupt render (time slicing)', async () => {
125 | await page.click('#transitionShowCounter');
126 | // wait until all counts become zero
127 | await Promise.all(ids.map(async (i) => {
128 | await expect(page).toMatchElement(`.count:nth-of-type(${i + 1})`, {
129 | text: '0',
130 | timeout: 5 * 1000,
131 | });
132 | }));
133 | const delays = [];
134 | for (let loop = 0; loop < REPEAT; loop += 1) {
135 | const start = Date.now();
136 | await page.click('#transitionIncrement');
137 | delays.push(Date.now() - start);
138 | await sleep(100);
139 | }
140 | console.log(name, delays);
141 | // check delays taken by clicking buttons in check1
142 | // each render takes at least 20ms and there are 50 components,
143 | // it triggers triple clicks, so 300ms on average.
144 | const avg = delays.reduce((a, b) => a + b) / delays.length;
145 | expect(avg).toBeLessThan(300);
146 | });
147 |
148 | it('Can branch state (wip state)', async () => {
149 | await page.click('#transitionShowCounter');
150 | await page.click('#transitionIncrement');
151 | await Promise.all(ids.map(async (i) => {
152 | await expect(page).toMatchElement(`.count:nth-of-type(${i + 1})`, {
153 | text: '1',
154 | timeout: 5 * 1000,
155 | });
156 | }));
157 | await page.click('#transitionIncrement');
158 | await sleep(100);
159 | await page.click('#transitionIncrement');
160 | // wait for pending
161 | await expect(page).toMatchElement('#pending', {
162 | text: 'Pending...',
163 | timeout: 2 * 1000,
164 | });
165 | // Make sure that while isPending true, previous state displayed
166 | await expect(page.evaluate(() => document.querySelector('#mainCount').innerHTML)).resolves.toBe('1');
167 | await expect(page.evaluate(() => document.querySelector('.count:first-of-type').innerHTML)).resolves.toBe('1');
168 | // click normal double button
169 | await page.click('#normalDouble');
170 | // check if all counts become doubled before increment
171 | await Promise.all(ids.map(async (i) => {
172 | await expect(page).toMatchElement(`.count:nth-of-type(${i + 1})`, {
173 | text: '2',
174 | timeout: 5 * 1000,
175 | });
176 | }));
177 | // check if all counts become doubled after increment
178 | await Promise.all(ids.map(async (i) => {
179 | await expect(page).toMatchElement(`.count:nth-of-type(${i + 1})`, {
180 | text: '6',
181 | timeout: 5 * 1000,
182 | });
183 | }));
184 | });
185 | });
186 | });
187 |
188 | describe('With useDeferredValue', () => {
189 | describe('Level 1', () => {
190 | it('No tearing finally on update', async () => {
191 | await page.click('#transitionShowDeferred');
192 | // wait until all counts become zero
193 | await Promise.all(ids.map(async (i) => {
194 | await expect(page).toMatchElement(`.count:nth-of-type(${i + 1})`, {
195 | text: '0',
196 | timeout: 5 * 1000,
197 | });
198 | }));
199 | for (let loop = 0; loop < REPEAT; loop += 1) {
200 | await page.click('#normalIncrement');
201 | await sleep(100);
202 | }
203 | // check if all counts become REPEAT
204 | await Promise.all(ids.map(async (i) => {
205 | await expect(page).toMatchElement(`.count:nth-of-type(${i + 1})`, {
206 | text: `${REPEAT}`,
207 | timeout: 10 * 1000,
208 | });
209 | }));
210 | });
211 |
212 | it('No tearing finally on mount', async () => {
213 | await page.click('#startAutoIncrement');
214 | await sleep(100);
215 | await page.click('#transitionShowDeferred');
216 | await sleep(1000);
217 | await page.click('#stopAutoIncrement');
218 | await sleep(2000);
219 | const count = page.evaluate(() => document.querySelector('.count:first-of-type'));
220 | await Promise.all(ids.map(async (i) => {
221 | await expect(page).toMatchElement(`.count:nth-of-type(${i + 1})`, {
222 | text: count,
223 | timeout: 10 * 1000,
224 | });
225 | }));
226 | });
227 | });
228 |
229 | describe('Level 2', () => {
230 | it('No tearing temporarily on update', async () => {
231 | await page.click('#transitionShowDeferred');
232 | // wait until all counts become zero
233 | await Promise.all(ids.map(async (i) => {
234 | await expect(page).toMatchElement(`.count:nth-of-type(${i + 1})`, {
235 | text: '0',
236 | timeout: 5 * 1000,
237 | });
238 | }));
239 | for (let loop = 0; loop < REPEAT; loop += 1) {
240 | await page.click('#normalIncrement');
241 | await sleep(100);
242 | }
243 | await sleep(5000);
244 | // check if there's inconsistency during update
245 | // see useCheckTearing() in src/common.js
246 | await expect(page.title()).resolves.not.toMatch(/TEARED/);
247 | });
248 |
249 | it('No tearing temporarily on mount', async () => {
250 | await page.click('#startAutoIncrement');
251 | await sleep(100);
252 | await page.click('#transitionShowDeferred');
253 | await sleep(1000);
254 | await page.click('#stopAutoIncrement');
255 | await sleep(2000);
256 | // check if there's inconsistency during update
257 | // see useCheckTearing() in src/common.js
258 | await expect(page.title()).resolves.not.toMatch(/TEARED/);
259 | });
260 | });
261 | });
262 | });
263 | });
264 |
--------------------------------------------------------------------------------
/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-all": "run-s build:*"
39 | },
40 | "keywords": [
41 | "react",
42 | "context",
43 | "hooks"
44 | ],
45 | "license": "MIT",
46 | "dependencies": {
47 | "@apollo/client": "^3.7.6",
48 | "@react-rxjs/core": "^0.10.3",
49 | "@simplux/core": "^0.18.0",
50 | "@simplux/react": "^0.18.0",
51 | "constate": "^3.3.2",
52 | "effector": "^22.5.0",
53 | "effector-react": "^22.4.0",
54 | "graphql": "^16.6.0",
55 | "jotai": "^2.0.0",
56 | "react": "^18.2.0",
57 | "react-dom": "^18.2.0",
58 | "react-hooks-global-state": "^2.1.0",
59 | "react-query": "^4.0.0-beta.3",
60 | "react-redux": "^8.0.5",
61 | "react-tracked": "^1.7.11",
62 | "recoil": "^0.7.6",
63 | "redux": "^4.2.1",
64 | "rxjs": "^7.8.0",
65 | "use-atom": "^0.9.0",
66 | "use-context-selector": "^1.4.1",
67 | "use-subscription": "^1.8.0",
68 | "valtio": "^1.9.0",
69 | "zustand": "^4.3.2"
70 | },
71 | "devDependencies": {
72 | "@babel/cli": "^7.20.7",
73 | "@babel/core": "^7.20.12",
74 | "@babel/preset-env": "^7.20.2",
75 | "@babel/preset-react": "^7.18.6",
76 | "babel-loader": "^9.1.2",
77 | "cross-env": "^7.0.3",
78 | "eslint": "^8.33.0",
79 | "eslint-config-airbnb": "^19.0.4",
80 | "eslint-plugin-import": "^2.27.5",
81 | "eslint-plugin-jsx-a11y": "^6.7.1",
82 | "eslint-plugin-react": "^7.32.2",
83 | "eslint-plugin-react-hooks": "^4.6.0",
84 | "html-webpack-plugin": "^5.5.0",
85 | "http-server": "^14.1.1",
86 | "jest": "^29.4.1",
87 | "jest-puppeteer": "^6.2.0",
88 | "npm-run-all": "^4.1.5",
89 | "puppeteer": "17.1.3",
90 | "webpack": "^5.75.0",
91 | "webpack-cli": "^5.0.1",
92 | "webpack-dev-server": "^4.11.1"
93 | },
94 | "babel": {
95 | "env": {
96 | "development": {
97 | "presets": [
98 | [
99 | "@babel/preset-env",
100 | {
101 | "targets": "> 0.2%, not dead"
102 | }
103 | ],
104 | "@babel/preset-react"
105 | ]
106 | },
107 | "jest": {
108 | "plugins": [
109 | "@babel/plugin-transform-modules-commonjs",
110 | "@babel/plugin-transform-react-jsx"
111 | ]
112 | }
113 | }
114 | },
115 | "jest": {
116 | "preset": "jest-puppeteer"
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/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 | ) => {
71 | const Counter = componentWrapper(() => {
72 | const count = useCount();
73 | syncBlock();
74 | return {count}
;
75 | });
76 |
77 | const DeferredCounter = componentWrapper(() => {
78 | const count = useDeferredValue(useCount());
79 | syncBlock();
80 | return {count}
;
81 | });
82 |
83 | const Main = () => {
84 | const [isPending, startTransition] = useTransition();
85 | const [mode, setMode] = useState(null);
86 | const transitionHide = () => {
87 | startTransition(() => setMode(null));
88 | };
89 | const transitionShowCounter = () => {
90 | startTransition(() => setMode('counter'));
91 | };
92 | const transitionShowDeferred = () => {
93 | startTransition(() => setMode('deferred'));
94 | };
95 | const count = useCount();
96 | const deferredCount = useDeferredValue(count);
97 | useCheckTearing();
98 | const increment = useIncrement();
99 | const doDouble = useDouble();
100 | const transitionIncrement = () => {
101 | startTransition(increment);
102 | };
103 | const timer = useRef();
104 | const stopAutoIncrement = () => {
105 | clearInterval(timer.current);
106 | };
107 | const startAutoIncrement = () => {
108 | stopAutoIncrement();
109 | timer.current = setInterval(increment, 50);
110 | };
111 | return (
112 |
113 |
116 |
119 |
122 |
125 |
128 |
131 |
134 |
137 |
{isPending && 'Pending...'}
138 |
Counters
139 | {mode === 'counter' && ids.map((id) =>
)}
140 | {mode === 'deferred' && ids.map((id) =>
)}
141 |
Main
142 |
{mode === 'deferred' ? deferredCount : count}
143 |
144 | );
145 | };
146 |
147 | const App = () => (
148 |
149 |
150 |
151 | );
152 |
153 | return App;
154 | };
155 |
--------------------------------------------------------------------------------
/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/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 | };
23 |
24 | const numTests = 10;
25 |
26 | function wrap(content, tag) { return `<${tag}>${content}${tag}>`; }
27 | function check(status) { return status === 'failed' ? ':x:' : ':white_check_mark:'; }
28 |
29 | // Get results into an array of test with a 2nd dimension by test/fail
30 | const results = JSON.parse(fs.readFileSync('./outfile.json', 'utf8'));
31 | const testResults = [];
32 | results.testResults[0].assertionResults.forEach((result, ix) => {
33 | const testNumber = Math.floor(ix / numTests);
34 | testResults[testNumber] = testResults[testNumber] || [];
35 | testResults[testNumber][ix % numTests] = {
36 | status: result.status,
37 | title: result.ancestorTitles[0],
38 | };
39 | });
40 |
41 | // Format table for substitution in outfile
42 | let sub = '';
43 | testResults.forEach((result) => {
44 | if (!libraries[result[0].title]) {
45 | console.info('no library entry for', result[0].title);
46 | return;
47 | }
48 | const th = wrap(libraries[result[0].title], 'th');
49 | const tds = result.map((test) => `\t\t${wrap(check(test.status), 'td')}\n`).join('');
50 | sub += `\t\n\t\t${th}\n${tds}\t
\n`;
51 | });
52 |
53 | // Find first and last line of raw results
54 | let first = 0;
55 | let last = 0;
56 | function note(line, ix) {
57 | if (line.match(/[✓✕]/)) {
58 | if (!first) first = ix - 6;
59 | last = ix;
60 | }
61 | // eslint-disable-next-line no-control-regex
62 | return line.replace(/..\r/g, '').replace(/\[.../g, '').substr(5);
63 | }
64 |
65 | // Read and process raw results
66 | const resultsRaw = fs.readFileSync('./outfile_raw.txt', 'utf8');
67 | let lines = resultsRaw.split(/\n\r|\n/).map((l, i) => note(l.substr(0), i));
68 | lines = lines.slice(first, last + 1);
69 | lines = lines.filter((line, ix) => ix % 3 === 0);
70 |
71 | // Update readme
72 | let readme = fs.readFileSync('./README.md', 'utf8');
73 | readme = readme.replace(
74 | /([\s\S]*?)<\/table>/,
75 | `\nTest | ${Array.from(Array(numTests).keys()).map((i) => `${i + 1} | `).join('')}
\n${sub}\n
`,
76 | );
77 | readme = readme.replace(
78 | /([\s\S]*?)<\/details>/,
79 | `\nRaw Output
\n\n\`\`\`\n${lines.join('\n')}\n\n\`\`\`\n `,
80 | );
81 |
82 | fs.writeFileSync('./README.md', readme);
83 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------