Files
react/packages/create-subscription
Andrew Clark a724a3b578 [RFC] Codemod invariant -> throw new Error (#22435)
* Hoist error codes import to module scope

When this code was written, the error codes map (`codes.json`) was
created on-the-fly, so we had to lazily require from inside the visitor.

Because `codes.json` is now checked into source, we can import it a
single time in module scope.

* Minify error constructors in production

We use a script to minify our error messages in production. Each message
is assigned an error code, defined in `scripts/error-codes/codes.json`.
Then our build script replaces the messages with a link to our
error decoder page, e.g. https://reactjs.org/docs/error-decoder.html/?invariant=92

This enables us to write helpful error messages without increasing the
bundle size.

Right now, the script only works for `invariant` calls. It does not work
if you throw an Error object. This is an old Facebookism that we don't
really need, other than the fact that our error minification script
relies on it.

So, I've updated the script to minify error constructors, too:

Input:
  Error(`A ${adj} message that contains ${noun}`);
Output:
  Error(formatProdErrorMessage(ERR_CODE, adj, noun));

It only works for constructors that are literally named Error, though we
could add support for other names, too.

As a next step, I will add a lint rule to enforce that errors written
this way must have a corresponding error code.

* Minify "no fallback UI specified" error in prod

This error message wasn't being minified because it doesn't use
invariant. The reason it didn't use invariant is because this particular
error is created without begin thrown — it doesn't need to be thrown
because it's located inside the error handling part of the runtime.

Now that the error minification script supports Error constructors, we
can minify it by assigning it a production error code in
`scripts/error-codes/codes.json`.

To support the use of Error constructors more generally, I will add a
lint rule that enforces each message has a corresponding error code.

* Lint rule to detect unminified errors

Adds a lint rule that detects when an Error constructor is used without
a corresponding production error code.

We already have this for `invariant`, but not for regular errors, i.e.
`throw new Error(msg)`. There's also nothing that enforces the use of
`invariant` besides convention.

There are some packages where we don't care to minify errors. These are
packages that run in environments where bundle size is not a concern,
like react-pg. I added an override in the ESLint config to ignore these.

* Temporarily add invariant codemod script

I'm adding this codemod to the repo temporarily, but I'll revert it
in the same PR. That way we don't have to check it in but it's still
accessible (via the PR) if we need it later.

* [Automated] Codemod invariant -> Error

This commit contains only automated changes:

npx jscodeshift -t scripts/codemod-invariant.js packages --ignore-pattern="node_modules/**/*"
yarn linc --fix
yarn prettier

I will do any manual touch ups in separate commits so they're easier
to review.

* Remove temporary codemod script

This reverts the codemod script and ESLint config I added temporarily
in order to perform the invariant codemod.

* Manual touch ups

A few manual changes I made after the codemod ran.

* Enable error code transform per package

Currently we're not consistent about which packages should have their
errors minified in production and which ones should.

This adds a field to the bundle configuration to control whether to
apply the transform. We should decide what the criteria is going
forward. I think it's probably a good idea to minify any package that
gets sent over the network. So yes to modules that run in the browser,
and no to modules that run on the server and during development only.
2021-09-30 12:01:28 -07:00
..

create-subscription

create-subscription is a utility for subscribing to external data sources inside React components. It is officially supported and maintained by the React team.

When should you NOT use this?

This utility should be used for subscriptions to a single value that are typically only read in one place and may update frequently (e.g. a component that subscribes to a geolocation API to show a dot on a map).

Other cases have better long-term solutions:

  • Redux/Flux stores should use the context API instead.
  • I/O subscriptions (e.g. notifications) that update infrequently should use react-cache instead.
  • Complex libraries like Relay/Apollo should manage subscriptions manually with the same techniques which this library uses under the hood (as referenced here) in a way that is most optimized for their library usage.

Limitations in async mode

The main motivation for create-subscription is to provide a way for library authors to ensure compatibility with React's upcoming asynchronous rendering mode. create-subscription guarantees correctness in async mode, accounting for the subtle bugs and edge cases that a library author might otherwise miss.

However, it achieves correctness by sometimes de-opting to synchronous mode, obviating the benefits of async rendering. This is an inherent limitation of storing state outside of React's managed state queue and rendering in response to a change event.

The effect of de-opting to sync mode is that the main thread may periodically be blocked (in the case of CPU-bound work), and placeholders may appear earlier than desired (in the case of IO-bound work).

For full compatibility with asynchronous rendering, including both time-slicing and React Suspense, the suggested longer-term solution is to move to one of the patterns described in the previous section.

What types of subscriptions can this support?

This abstraction can handle a variety of subscription types, including:

  • Event dispatchers like HTMLInputElement.
  • Custom pub/sub components like Relay's FragmentSpecResolver.
  • Observable types like RxJS BehaviorSubject and ReplaySubject. (Types like RxJS Subject or Observable are not supported, because they provide no way to read the "current" value after it has been emitted.)
  • Native Promises.

Installation

# Yarn
yarn add create-subscription

# NPM
npm install create-subscription

Usage

To configure a subscription, you must provide two methods: getCurrentValue and subscribe.

import { createSubscription } from "create-subscription";

const Subscription = createSubscription({
  getCurrentValue(source) {
    // Return the current value of the subscription (source),
    // or `undefined` if the value can't be read synchronously (e.g. native Promises).
  },
  subscribe(source, callback) {
    // Subscribe (e.g. add an event listener) to the subscription (source).
    // Call callback(newValue) whenever a subscription changes.
    // Return an unsubscribe method,
    // Or a no-op if unsubscribe is not supported (e.g. native Promises).
  }
});

To use the Subscription component, pass the subscribable property (e.g. an event dispatcher, observable) as the source property and use a render prop, children, to handle the subscribed value when it changes:

<Subscription source={eventDispatcher}>
  {value => <AnotherComponent value={value} />}
</Subscription>

Examples

This API can be used to subscribe to a variety of "subscribable" sources, from event dispatchers to RxJS observables. Below are a few examples of how to subscribe to common types.

Subscribing to event dispatchers

Below is an example showing how create-subscription can be used to subscribe to event dispatchers such as DOM elements.

import React from "react";
import { createSubscription } from "create-subscription";

// Start with a simple component.
// In this case, it's a function component, but it could have been a class.
function FollowerComponent({ followersCount }) {
  return <div>You have {followersCount} followers!</div>;
}

// Create a wrapper component to manage the subscription.
const EventHandlerSubscription = createSubscription({
  getCurrentValue: eventDispatcher => eventDispatcher.value,
  subscribe: (eventDispatcher, callback) => {
    const onChange = event => callback(eventDispatcher.value);
    eventDispatcher.addEventListener("change", onChange);
    return () => eventDispatcher.removeEventListener("change", onChange);
  }
});

// Your component can now be used as shown below.
// In this example, 'eventDispatcher' represents a generic event dispatcher.
<EventHandlerSubscription source={eventDispatcher}>
  {value => <FollowerComponent followersCount={value} />}
</EventHandlerSubscription>;

Subscribing to observables

Below are examples showing how create-subscription can be used to subscribe to certain types of observables (e.g. RxJS BehaviorSubject and ReplaySubject).

Note that it is not possible to support all observable types (e.g. RxJS Subject or Observable) because some provide no way to read the "current" value after it has been emitted.

BehaviorSubject

const BehaviorSubscription = createSubscription({
  getCurrentValue: behaviorSubject => behaviorSubject.getValue(),
  subscribe: (behaviorSubject, callback) => {
    const subscription = behaviorSubject.subscribe(callback);
    return () => subscription.unsubscribe();
  }
});

ReplaySubject

const ReplaySubscription = createSubscription({
  getCurrentValue: replaySubject => {
    let currentValue;
    // ReplaySubject does not have a sync data getter,
    // So we need to temporarily subscribe to retrieve the most recent value.
    replaySubject
      .subscribe(value => {
        currentValue = value;
      })
      .unsubscribe();
    return currentValue;
  },
  subscribe: (replaySubject, callback) => {
    const subscription = replaySubject.subscribe(callback);
    return () => subscription.unsubscribe();
  }
});

Subscribing to a Promise

Below is an example showing how create-subscription can be used with native Promises.

Note that an initial render value of undefined is unavoidable due to the fact that Promises provide no way to synchronously read their current value.

Note the lack of a way to "unsubscribe" from a Promise can result in memory leaks as long as something has a reference to the Promise. This should be taken into consideration when determining whether Promises are appropriate to use in this way within your application.

import React from "react";
import { createSubscription } from "create-subscription";

// Start with a simple component.
function LoadingComponent({ loadingStatus }) {
  if (loadingStatus === undefined) {
    // Loading
  } else if (loadingStatus === null) {
    // Error
  } else {
    // Success
  }
}

// Wrap the function component with a subscriber HOC.
// This HOC will manage subscriptions and pass values to the decorated component.
// It will add and remove subscriptions in an async-safe way when props change.
const PromiseSubscription = createSubscription({
  getCurrentValue: promise => {
    // There is no way to synchronously read a Promise's value,
    // So this method should return undefined.
    return undefined;
  },
  subscribe: (promise, callback) => {
    promise.then(
      // Success
      value => callback(value),
      // Failure
      () => callback(null)
    );

    // There is no way to "unsubscribe" from a Promise.
    // create-subscription will still prevent stale values from rendering.
    return () => {};
  }
});

// Your component can now be used as shown below.
<PromiseSubscription source={loadingPromise}>
  {loadingStatus => <LoadingComponent loadingStatus={loadingStatus} />}
</PromiseSubscription>