Like this demo? Subscribe for a new one each month →

Tearable Dots - a state tearing attempt tested against different global state solutions in React 18

Demo at the bottom

Summary: An interactive demo modelled after @rickhanlonii's discussion on github and inspired by Tanner Linsley's post on twitter about the same topic: https://twitter.com/tannerlinsley/s...

You're using the following React render mode:

concurrent

You're using the following global state management strategy:

external store tracked via `useEffect` and `useState`

The demo below renders 3 dots, each of which include render blocking code to simulate expensive renders, and a button to toggle the colors of the dots. The idea is to attempt a state tearing update in between the first and second dot renders. If the tearing attempt worked, the first dot's final color will not match the color of the second two dots.

It's important to note that state tearing will only occur in one of the scenarios, but I included several other global state strategies to show how their solutions introduce new tradeoffs. For example, React's new useSyncExternalStore does not support the transition API and falls back to synchronous rendering if inconsistencies are detected. This is visible in the demo because the button never turns grey after the user clicks it.

Change the strategy/mode by clicking a link below:

render mode: concurrent - strategy: external store tracked via `useEffect` and `useState`

This works in sync mode but will cause tearing in concurrent mode. Because it supports the transition API, the tearing attempt will occur mid-render instead of waiting for the the dot renders to complete.

render mode: concurrent - strategy: external store tracked via useSyncExternalStore

This is the new way of managing external global state via `useSyncExternalStore` and will NOT cause tearing. The tradeoff is that `useSyncExternalStore` will fallback to rendering in sync mode if it detects inconsistencies. It also will not work with React's new transition APIs. This is easy to see because the button never turns grey after the user clicks it.

render mode: concurrent - strategy: store managed withing React's context API

This uses React's context API to manage state and will NOT cause tearing. It also works with React's new transition APIs. The reason why this approach is not more popular, though, is because React's `useContext` does not allow fine-grained reactivity and will cause unnecessary rerenders in components that only need to access a slice of the store.

render mode: sync - strategy: external store tracked via `useEffect` and `useState`

This works because it does not make use of the transition API and will wait for the dot renders to complete before making the tearing attempt.

render mode: sync - strategy: external store tracked via useSyncExternalStore

This works in synchronous mode but isn't necessary since synchronous renders should result in the consistent state.

render mode: sync - strategy: store managed withing React's context API

This uses React's context API to manage global state and will NOT cause tearing. In sync mode (and probably in concurrent mode too), this is never a recommended way of managing global state due to rerender concerns.

Important: the lifecycle events below the dots section will tell you exactly what is happening with the render lifecyle under the hood.

Current strategy: external store tracked via `useEffect` and `useState`

Will tear: true

Supports React transition API: true

This works in sync mode but will cause tearing in concurrent mode. Because it supports the transition API, the tearing attempt will occur mid-render instead of waiting for the the dot renders to complete.

Lifecycle Event Log

393 - running React in concurrent mode
394 - with strategy: external store tracked via `useEffect` and `useState`
405 - app mounted
415 - app rendered