├── .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 | Preview 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 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 |
Test12345678910
react-redux:white_check_mark::white_check_mark::white_check_mark::white_check_mark::x::x::white_check_mark::white_check_mark::white_check_mark::white_check_mark:
zustand:white_check_mark::white_check_mark::white_check_mark::white_check_mark::x::x::white_check_mark::white_check_mark::white_check_mark::white_check_mark:
react-tracked:white_check_mark::white_check_mark::white_check_mark::white_check_mark::white_check_mark::white_check_mark::white_check_mark::white_check_mark::white_check_mark::white_check_mark:
constate:white_check_mark::white_check_mark::white_check_mark::white_check_mark::white_check_mark::white_check_mark::white_check_mark::white_check_mark::white_check_mark::white_check_mark:
react-hooks-global-state:white_check_mark::white_check_mark::white_check_mark::white_check_mark::x::x::white_check_mark::white_check_mark::white_check_mark::white_check_mark:
use-context-selector (w/ useReducer, w/o useContextUpdate):white_check_mark::white_check_mark::white_check_mark::white_check_mark::x::x::white_check_mark::white_check_mark::white_check_mark::white_check_mark:
use-context-selector (w/ useReducer):white_check_mark::white_check_mark::white_check_mark::white_check_mark::white_check_mark::white_check_mark::white_check_mark::white_check_mark::white_check_mark::white_check_mark:
use-subscription (w/ redux):white_check_mark::white_check_mark::white_check_mark::white_check_mark::x::x::white_check_mark::white_check_mark::white_check_mark::white_check_mark:
apollo-client:white_check_mark::white_check_mark::white_check_mark::white_check_mark::x::x::white_check_mark::white_check_mark::white_check_mark::white_check_mark:
recoil:white_check_mark::white_check_mark::white_check_mark::white_check_mark::x::x::white_check_mark::white_check_mark::white_check_mark::white_check_mark:
recoil (UNSTABLE):white_check_mark::white_check_mark::white_check_mark::x::white_check_mark::x::white_check_mark::white_check_mark::white_check_mark::x:
jotai:white_check_mark::white_check_mark::white_check_mark::x::white_check_mark::x::white_check_mark::white_check_mark::white_check_mark::x:
use-atom:white_check_mark::white_check_mark::white_check_mark::white_check_mark::white_check_mark::white_check_mark::white_check_mark::white_check_mark::white_check_mark::white_check_mark:
valtio:white_check_mark::white_check_mark::white_check_mark::white_check_mark::x::x::white_check_mark::white_check_mark::white_check_mark::white_check_mark:
effector:white_check_mark::white_check_mark::white_check_mark::white_check_mark::x::x::white_check_mark::white_check_mark::white_check_mark::white_check_mark:
react-rxjs:white_check_mark::white_check_mark::white_check_mark::white_check_mark::x::x::white_check_mark::white_check_mark::white_check_mark::white_check_mark:
simplux:white_check_mark::white_check_mark::white_check_mark::white_check_mark::white_check_mark::x::white_check_mark::white_check_mark::white_check_mark::white_check_mark:
react-query:white_check_mark::white_check_mark::x::white_check_mark::x::x::white_check_mark::white_check_mark::white_check_mark::white_check_mark:
mobx-react-lite:white_check_mark::white_check_mark::white_check_mark::white_check_mark::x::x::white_check_mark::white_check_mark::white_check_mark::white_check_mark:
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}`; } 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 | `
\n${Array.from(Array(numTests).keys()).map((i) => ``).join('')}\n${sub}\n
Test${i + 1}
`, 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 | --------------------------------------------------------------------------------