├── .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 | 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 (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 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 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 |
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:
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}`; } 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 | `
\n${Array.from(Array(numTests).keys()).map((i) => ``).join('')}\n${sub}\n
Test${i + 1}
`, 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 | --------------------------------------------------------------------------------