Checking tearing in React concurrent mode
In react-redux, there's a theoretical issue called "tearing" that might occur in React concurrent mode.
Let's try to check it!
A small app is implemented with each library. The state has one count. The app shows the count in fifty components.
There's a button outside of React and if it's clicked it will trigger state mutation. This is to emulate mutating an external state outside of React, for example updating state by Redux middleware.
The render has intentionaly expensive computation. If the mutation happens during rendering with in a tree, there could be an inconsistency in the state. If it finds the inconsistency, the test will fail.
git clone https://github.com/dai-shi/will-this-react-global-state-work-in-concurrent-mode.git
cd will-this-react-global-state-work-in-concurrent-mode
npm install
npm run build-all
PORT=8080 npm run http-server &
PORT=8080 npm run jest
You can also test it by opening http://localhost:8080/react-redux
in your browser, and click the button very quickly. (check the console log)
Raw Output
react-redux
check with events from outside
✓ check 1: updated properly (3201ms)
✕ check 2: no tearing during update (23ms)
✓ check 3: ability to interrupt render
✓ check 4: proper update after interrupt (1387ms)
check with useTransaction
✓ check 5: updated properly with transition (2527ms)
✕ check 6: no tearing with transition (2ms)
✕ check 7: proper branching with transition (5444ms)
reactive-react-redux
check with events from outside
✓ check 1: updated properly (3169ms)
✓ check 2: no tearing during update (1ms)
✓ check 3: ability to interrupt render
✓ check 4: proper update after interrupt (1179ms)
check with useTransaction
✓ check 5: updated properly with transition (2462ms)
✓ check 6: no tearing with transition (1ms)
✕ check 7: proper branching with transition (7450ms)
react-tracked
check with events from outside
✓ check 1: updated properly (7851ms)
✓ check 2: no tearing during update (1ms)
✓ check 3: ability to interrupt render
✓ check 4: proper update after interrupt (2323ms)
check with useTransaction
✓ check 5: updated properly with transition (3531ms)
✓ check 6: no tearing with transition (1ms)
✓ check 7: proper branching with transition (3584ms)
constate
check with events from outside
✓ check 1: updated properly (8586ms)
✓ check 2: no tearing during update (1ms)
✓ check 3: ability to interrupt render
✓ check 4: proper update after interrupt (2325ms)
check with useTransaction
✓ check 5: updated properly with transition (4671ms)
✓ check 6: no tearing with transition (1ms)
✓ check 7: proper branching with transition (4533ms)
zustand
check with events from outside
✓ check 1: updated properly (3181ms)
✕ check 2: no tearing during update (21ms)
✓ check 3: ability to interrupt render
✓ check 4: proper update after interrupt (1413ms)
check with useTransaction
✓ check 5: updated properly with transition (2544ms)
✕ check 6: no tearing with transition (2ms)
✕ check 7: proper branching with transition (5426ms)
react-sweet-state
check with events from outside
✓ check 1: updated properly (11104ms)
✕ check 2: no tearing during update (1ms)
✕ check 3: ability to interrupt render
✓ check 4: proper update after interrupt (2344ms)
check with useTransaction
✕ check 5: updated properly with transition (3968ms)
✕ check 6: no tearing with transition (37ms)
✕ check 7: proper branching with transition (8684ms)
storeon
check with events from outside
✓ check 1: updated properly (3152ms)
✕ check 2: no tearing during update (21ms)
✓ check 3: ability to interrupt render
✓ check 4: proper update after interrupt (1438ms)
check with useTransaction
✕ check 5: updated properly with transition (2658ms)
✓ check 6: no tearing with transition (19ms)
✕ check 7: proper branching with transition (7419ms)
react-hooks-global-state
check with events from outside
✓ check 1: updated properly (8612ms)
✓ check 2: no tearing during update (1ms)
✓ check 3: ability to interrupt render
✓ check 4: proper update after interrupt (1099ms)
check with useTransaction
✓ check 5: updated properly with transition (3500ms)
✓ check 6: no tearing with transition (1ms)
✕ check 7: proper branching with transition (7335ms)
use-context-selector
check with events from outside
✓ check 1: updated properly (8620ms)
✓ check 2: no tearing during update (2ms)
✓ check 3: ability to interrupt render
✓ check 4: proper update after interrupt (2380ms)
check with useTransaction
✓ check 5: updated properly with transition (3532ms)
✓ check 6: no tearing with transition (2ms)
✓ check 7: proper branching with transition (2693ms)
mobx-react-lite
check with events from outside
✓ check 1: updated properly (2836ms)
✕ check 2: no tearing during update (1ms)
✓ check 3: ability to interrupt render (1ms)
✓ check 4: proper update after interrupt (1247ms)
check with useTransaction
✓ check 5: updated properly with transition (2629ms)
✕ check 6: no tearing with transition (2ms)
✕ check 7: proper branching with transition (5583ms)
use-subscription
check with events from outside
✓ check 1: updated properly (8579ms)
✓ check 2: no tearing during update (1ms)
✓ check 3: ability to interrupt render
✓ check 4: proper update after interrupt (2440ms)
check with useTransaction
✓ check 5: updated properly with transition (4536ms)
✓ check 6: no tearing with transition (1ms)
✕ check 7: proper branching with transition (7426ms)
check 1: updated properly | check 2: no tearing during update | check 3: ability to interrupt render | check 4: proper update after interrupt | check 5: updated properly with transition | check 6: no tearing with transition | check 7: proper branching with transition | |
---|---|---|---|---|---|---|---|
react-redux | Pass | Fail | Pass | Pass | Pass | Fail | Fail |
reactive-react-redux | Pass | Pass | Pass | Pass | Pass | Pass | Fail |
react-tracked | Pass | Pass | Pass | Pass | Pass | Pass | Pass |
constate | Pass | Pass | Pass | Pass | Pass | Pass | Pass |
zustand | Pass | Fail | Pass | Pass | Pass | Fail | Fail |
react-sweet-state | Pass | Fail | Fair (just a little bit slow) | Pass | Fail (slow pending) | Fail | Fail |
storeon | Pass | Fail | Pass | Pass | Fail (slow pending) | Pass | Fail |
react-hooks-global-state | Pass | Pass | Pass | Pass | Pass | Pass | Fail |
use-context-selector (w/ useReducer) | Pass | Pass | Pass | Pass | Pass | Pass | Pass |
mobx-react-lite | Pass | Fail | Pass | Pass | Pass | Fail | Fail |
use-subscription (w/ redux) | Pass | Pass | Pass | Pass | Pass | Pass | Fail |
Do no believe the result too much. The test is done in a very limited way. Something might be wrong.
We are not yet sure what the final conurrent mode would be. It's likely that there could be some issues other than "tearing."
The reason why I created this is to promote my projects!
The feature of these libraries is not only concurrent mode friendly, but also state usage tracking.