A RetroSearch Logo

Home - News ( United States | United Kingdom | Italy | Germany ) - Football scores

Search Query:

Showing content from https://github.com/reactwg/react-18/discussions/86 below:

useMutableSource → useSyncExternalStore · reactwg/react-18 · Discussion #86 · GitHub

Since experimental useMutableSource API was added, we’ve made changes to our overall concurrent rendering model that have led us to reconsider its design. Members of this Working Group have also reported flaws with the existing API contract that make it difficult for library maintainers to adopt useMutableSource in their implementations.

After additional research and discussions, here are our proposed changes to the API, which we intend to complete for React 18.

(For background, refer to the RFC for useMutableSource: https://github.com/reactjs/rfcs/blob/master/text/0147-use-mutable-source.md)

API overview
import {useSyncExternalStore} from 'react';

// We will also publish a backwards compatible shim
// It will prefer the native API, when available
import {useSyncExternalStore} from 'use-sync-external-store/shim';

// Basic usage. getSnapshot must return a cached/memoized result
const state = useSyncExternalStore(store.subscribe, store.getSnapshot);

// Selecting a specific field using an inline getSnapshot
const selectedField = useSyncExternalStore(store.subscribe, () => store.getSnapshot().selectedField);

getSnapshot is used to check if the subscribed value has changed since the last time it was rendered, so the result needs to be referentially stable. That means it either needs to be an immutable value like a string or number, or it needs to be a cached/memoized object.

As a convenience, we will provide a version of the API with automatic support for memoizing the result of getSnapshot:

// Name of API is not final
import {useSyncExternalStoreWithSelector} from 'use-sync-external-store/with-selector';
const selection = useSyncExternalStoreWithSelector(
  store.subscribe,
  store.getSnapshot,
  getServerSnapshot,
  selector,
  isEqual
);
Selectors no longer need to be memoized

Inline selectors are a common feature of state libraries, especially those that provide a hook-based API, such as Redux’s useSelector.

A selector is a function that accepts a state value and “selects” the subset that is needed by the component. It signals that React only needs to re-render if that subset has changed.

Because useMutableSource does not provide a built-in selector API, the only way to implement this behavior is to resubscribe to the store every time the inline selector changes. If the selector function is not memoized, this means resubscribing on every new render. This is not only a performance pitfall, it’s one that leaks into user code: the user of a state management library must take extra caution to memoize all of its selectors.

Refer to #84 for a detailed description of this problem.

The new API is designed so that React no longer needs to resubscribe when the getSnapshot function changes. So you can pass an unmemoized selector without degrading performance.

By default, React will detect changes to a selected value by comparing with Object.is. Some state libraries rely on comparing values with a custom comparison function, like shallowEqual. While we don’t necessarily recommend this pattern, it can be implemented in userspace. Since we expect this to be a common feature request, we will publish our own userspace implementation.

Concurrent reads, synchronous updates

Another flaw of the useMutableSource API is that it can sometimes cause visible parts of the UI to be replaced with a fallback, even when the update is wrapped with startTransition (the API that is meant to avoid this scenario).

The reason is that startTransition relies on the ability to maintain multiple versions of a UI simultaneously (“concurrently”): the current UI that’s visible on screen, and a work-in-progress UI that is prepared in the background while data progressively streams in. React can do this with its built-in state APIs — useState and useReducer — but not for state that lives outside React, because we only can access a single version of state at a time. (To illustrate with an example, Redux’s store has a getState method, but it doesn’t have a getBackgroundState method; we could theoretically implement a contract to support concurrent data stores, but that’s outside the scope of this proposal.)

Our original strategy was to provide partial support for concurrent features: start rendering concurrently, and only deopt back to synchronous when we detect an inconsistency. “Deopt" in this context can mean:

  1. Disabling time-slicing and reverting to fully synchronous, blocking rendering
  2. During a refresh transition, hiding the UI and replacing it with a fallback instead of waiting for the new data to load in the background

We went through great pains to preserve time-slicing as much as possible (1), even if it meant hiding already-visible UI with a fallback (2).

We’ve since concluded that this trade-off is backwards: Replacing visible content with a fallback is a significant regression in the user experience, especially if it happens unpredictably. By contrast, occasionally disabling time-slicing — while not ideal — has a much less dramatic effect on the end user experience.

Even worse is that useMutableSource can cause bad fallbacks during state updates that are completely unrelated, such as those triggered by a router, or a regular useState hook.

As we were brainstorming alternative strategies, a key revelation was that if you can’t rely on startTransition to always avoid bad fallbacks caused by a store update, then you shouldn’t ever rely on it — you should avoid the fallbacks in some other way. For example, you could do it the same way you would today without Suspense: by waiting for new data to load before triggering the update.

The next key revelation was that we can avoid deopts during updates triggered by React state transitions if updates triggered by external stores are always synchronous. That’s because if updates to stores are synchronous, they are guaranteed to be consistent.

So, in the new design:

We think this hits the sweet spot of feature compatibility, ease of adoption, and predictable user experience.

We will be renaming the API to useSyncExternalStore to reflect its updated behavior.

As a bonus, we can get rid of the source argument and the corresponding createMutableSource API.

Adoption strategy

Our goal is for all subscription-based libraries to migrate their implementations to useSyncExternalStore.

While it’s possible to implement a concurrent-compatible subscription in userspace, it’s very tricky to get right. Existing store implementations will continue to work as they do in React 17 until or unless a store update is wrapped with startTransition, at which point concurrency bugs may surface.

To encourage adoption by open source libraries, we will provide a shim that is compatible with older versions of React. The shim will prefer the built-in API when it is available, so that users get the correct implementation regardless of which version they’re running.

We are also considering a heuristic to detect when a userspace store update is wrapped with startTransition, so we can print advice to the console (in development mode) to use useSyncExternalStore instead. The heuristic is to count how many separate components are updated within a single startTransition call. If it’s greater than some arbitrary threshold, say 20, we can infer that it likely contains a subscription.


RetroSearch is an open source project built by @garambo | Open a GitHub Issue

Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo

HTML: 3.2 | Encoding: UTF-8 | Version: 0.7.4