diff --git a/packages/use-subscription/README.md b/packages/use-subscription/README.md
new file mode 100644
index 0000000000000..b56023d1c6e3c
--- /dev/null
+++ b/packages/use-subscription/README.md
@@ -0,0 +1,132 @@
+# use-subscription
+
+React hook that safely manages subscriptions in concurrent mode.
+
+## 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](https://reactjs.org/docs/context.html) instead.
+* I/O subscriptions (e.g. notifications) that update infrequently should use a mechanism like [`react-cache`](https://github.com/facebook/react/blob/master/packages/react-cache/README.md) instead.
+* Complex libraries like Relay/Apollo should manage subscriptions manually with the same techniques which this library uses under the hood (as referenced [here](https://gist.github.com/bvaughn/d569177d70b50b58bff69c3c4a5353f3)) in a way that is most optimized for their library usage.
+
+## Limitations in concurrent mode
+
+`use-subscription` is safe to use in concurrent mode. However, [it achieves correctness by sometimes de-opting to synchronous mode](https://github.com/facebook/react/issues/13186#issuecomment-403959161), obviating the benefits of concurrent 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 concurrent 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.)
+
+Note that JavaScript promises are also **not supported** because they provide no way to synchronously read the "current" value.
+
+# Installation
+
+```sh
+# Yarn
+yarn add use-subscription
+
+# NPM
+npm install use-subscription
+```
+
+# Usage
+
+To configure a subscription, you must provide two methods: `getCurrentValue` and `subscribe`.
+
+In order to avoid removing and re-adding subscriptions each time this hook is called, the parameters passed to this hook should be memoized. This can be done by wrapping the entire subscription with `useMemo()`, or by wrapping the individual callbacks with `useCallback()`.
+
+## Subscribing to event dispatchers
+
+Below is an example showing how `use-subscription` can be used to subscribe to event dispatchers such as DOM elements.
+
+```js
+import React, { useMemo } from "react";
+import { useSubscription } from "use-subscription";
+
+// In this example, "input" is an event dispatcher (e.g. an HTMLInputElement)
+// but it could be anything that emits an event and has a readable current value.
+function Example({ input }) {
+
+ // Memoize to avoid removing and re-adding subscriptions each time this hook is called.
+ const subscription = useMemo(
+ () => ({
+ getCurrentValue: () => input.value,
+ subscribe: callback => {
+ input.addEventListener("change", callback);
+ return () => input.removeEventListener("change", callback);
+ }
+ }),
+
+ // Re-subscribe any time our input changes
+ // (e.g. we get a new HTMLInputElement prop to subscribe to)
+ [input]
+ );
+
+ // The value returned by this hook reflects the input's current value.
+ // Our component will automatically be re-rendered when that value changes.
+ const value = useSubscription(subscription);
+
+ // Your rendered output goes here ...
+}
+```
+
+## Subscribing to observables
+
+Below are examples showing how `use-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`
+```js
+const subscription = useMemo(
+ () => ({
+ getCurrentValue: () => behaviorSubject.getValue(),
+ subscribe: callback => {
+ const subscription = behaviorSubject.subscribe(callback);
+ return () => subscription.unsubscribe();
+ }
+ }),
+
+ // Re-subscribe any time the behaviorSubject changes
+ [behaviorSubject]
+);
+
+const value = useSubscription(subscription);
+```
+
+### `ReplaySubject`
+```js
+const subscription = useMemo(
+ () => ({
+ getCurrentValue: () => {
+ 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: callback => {
+ const subscription = replaySubject.subscribe(callback);
+ return () => subscription.unsubscribe();
+ }
+ }),
+
+ // Re-subscribe any time the replaySubject changes
+ [replaySubject]
+);
+
+const value = useSubscription(subscription);
+```
diff --git a/packages/use-subscription/index.js b/packages/use-subscription/index.js
new file mode 100644
index 0000000000000..f5030786a9639
--- /dev/null
+++ b/packages/use-subscription/index.js
@@ -0,0 +1,12 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+'use strict';
+
+export * from './src/useSubscription';
diff --git a/packages/use-subscription/npm/index.js b/packages/use-subscription/npm/index.js
new file mode 100644
index 0000000000000..b91e9c4a14237
--- /dev/null
+++ b/packages/use-subscription/npm/index.js
@@ -0,0 +1,7 @@
+'use strict';
+
+if (process.env.NODE_ENV === 'production') {
+ module.exports = require('./cjs/use-subscription.production.min.js');
+} else {
+ module.exports = require('./cjs/use-subscription.development.js');
+}
diff --git a/packages/use-subscription/package.json b/packages/use-subscription/package.json
new file mode 100644
index 0000000000000..2643fbd161d1f
--- /dev/null
+++ b/packages/use-subscription/package.json
@@ -0,0 +1,24 @@
+{
+ "private": true,
+ "name": "use-subscription",
+ "description": "Reusable hooks",
+ "version": "0.0.0",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/facebook/react.git",
+ "directory": "packages/use-subscription"
+ },
+ "files": [
+ "LICENSE",
+ "README.md",
+ "build-info.json",
+ "index.js",
+ "cjs/"
+ ],
+ "peerDependencies": {
+ "react": "^16.8.0"
+ },
+ "devDependencies": {
+ "rxjs": "^5.5.6"
+ }
+}
diff --git a/packages/use-subscription/src/__tests__/useSubscription-test.internal.js b/packages/use-subscription/src/__tests__/useSubscription-test.internal.js
new file mode 100644
index 0000000000000..daacfb5cfe575
--- /dev/null
+++ b/packages/use-subscription/src/__tests__/useSubscription-test.internal.js
@@ -0,0 +1,563 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+let act;
+let useSubscription;
+let BehaviorSubject;
+let React;
+let ReactTestRenderer;
+let Scheduler;
+let ReplaySubject;
+
+describe('useSubscription', () => {
+ beforeEach(() => {
+ jest.resetModules();
+ jest.mock('scheduler', () => require('scheduler/unstable_mock'));
+
+ useSubscription = require('use-subscription').useSubscription;
+ React = require('react');
+ ReactTestRenderer = require('react-test-renderer');
+ Scheduler = require('scheduler');
+
+ act = ReactTestRenderer.act;
+
+ BehaviorSubject = require('rxjs').BehaviorSubject;
+ ReplaySubject = require('rxjs').ReplaySubject;
+ });
+
+ function createBehaviorSubject(initialValue) {
+ const behaviorSubject = new BehaviorSubject();
+ if (initialValue) {
+ behaviorSubject.next(initialValue);
+ }
+ return behaviorSubject;
+ }
+
+ function createReplaySubject(initialValue) {
+ const replaySubject = new ReplaySubject();
+ if (initialValue) {
+ replaySubject.next(initialValue);
+ }
+ return replaySubject;
+ }
+
+ it('supports basic subscription pattern', () => {
+ function Child({value = 'default'}) {
+ Scheduler.unstable_yieldValue(value);
+ return null;
+ }
+
+ function Subscription({source}) {
+ const value = useSubscription(
+ React.useMemo(
+ () => ({
+ getCurrentValue: () => source.getValue(),
+ subscribe: callback => {
+ const subscription = source.subscribe(callback);
+ return () => subscription.unsubscribe();
+ },
+ }),
+ [source],
+ ),
+ );
+ return ;
+ }
+
+ const observable = createBehaviorSubject();
+ let renderer;
+ act(() => {
+ renderer = ReactTestRenderer.create(
+ ,
+ {unstable_isConcurrent: true},
+ );
+ });
+ expect(Scheduler).toHaveYielded(['default']);
+
+ // Updates while subscribed should re-render the child component
+ act(() => observable.next(123));
+ expect(Scheduler).toHaveYielded([123]);
+ act(() => observable.next('abc'));
+ expect(Scheduler).toHaveYielded(['abc']);
+
+ // Unmounting the subscriber should remove listeners
+ act(() => renderer.update(
));
+ act(() => observable.next(456));
+ expect(Scheduler).toFlushAndYield([]);
+ });
+
+ it('should support observable types like RxJS ReplaySubject', () => {
+ function Child({value = 'default'}) {
+ Scheduler.unstable_yieldValue(value);
+ return null;
+ }
+
+ function Subscription({source}) {
+ const value = useSubscription(
+ React.useMemo(
+ () => ({
+ getCurrentValue: () => {
+ let currentValue;
+ source
+ .subscribe(tempValue => {
+ currentValue = tempValue;
+ })
+ .unsubscribe();
+ return currentValue;
+ },
+ subscribe: callback => {
+ const subscription = source.subscribe(callback);
+ return () => subscription.unsubscribe();
+ },
+ }),
+ [source],
+ ),
+ );
+ return ;
+ }
+
+ let observable = createReplaySubject('initial');
+ let renderer;
+ act(() => {
+ renderer = ReactTestRenderer.create(
+ ,
+ {unstable_isConcurrent: true},
+ );
+ });
+ expect(Scheduler).toHaveYielded(['initial']);
+ act(() => observable.next('updated'));
+ expect(Scheduler).toHaveYielded(['updated']);
+
+ Scheduler.unstable_flushAll();
+
+ // Unsetting the subscriber prop should reset subscribed values
+ observable = createReplaySubject(undefined);
+ act(() => renderer.update());
+ expect(Scheduler).toHaveYielded(['default']);
+ });
+
+ it('should unsubscribe from old sources and subscribe to new sources when memoized props change', () => {
+ function Child({value = 'default'}) {
+ Scheduler.unstable_yieldValue(value);
+ return null;
+ }
+
+ let subscriptions = [];
+
+ function Subscription({source}) {
+ const value = useSubscription(
+ React.useMemo(
+ () => ({
+ getCurrentValue: () => source.getValue(),
+ subscribe: callback => {
+ subscriptions.push(source);
+ const subscription = source.subscribe(callback);
+ return () => subscription.unsubscribe();
+ },
+ }),
+ [source],
+ ),
+ );
+ return ;
+ }
+
+ const observableA = createBehaviorSubject('a-0');
+ const observableB = createBehaviorSubject('b-0');
+
+ expect(subscriptions).toHaveLength(0);
+
+ let renderer;
+ act(() => {
+ renderer = ReactTestRenderer.create(
+ ,
+ {unstable_isConcurrent: true},
+ );
+ });
+
+ // Updates while subscribed should re-render the child component
+ expect(Scheduler).toHaveYielded(['a-0']);
+ expect(subscriptions).toHaveLength(1);
+ expect(subscriptions[0]).toBe(observableA);
+
+ // Unsetting the subscriber prop should reset subscribed values
+ act(() => renderer.update());
+
+ expect(Scheduler).toHaveYielded(['b-0']);
+ expect(subscriptions).toHaveLength(2);
+ expect(subscriptions[1]).toBe(observableB);
+
+ // Updates to the old subscribable should not re-render the child component
+ act(() => observableA.next('a-1'));
+ expect(Scheduler).toFlushAndYield([]);
+
+ // Updates to the bew subscribable should re-render the child component
+ act(() => observableB.next('b-1'));
+ expect(Scheduler).toHaveYielded(['b-1']);
+
+ expect(subscriptions).toHaveLength(2);
+ });
+
+ it('should unsubscribe from old sources and subscribe to new sources when useCallback functions change', () => {
+ function Child({value = 'default'}) {
+ Scheduler.unstable_yieldValue(value);
+ return null;
+ }
+
+ let subscriptions = [];
+
+ function Subscription({source}) {
+ const value = useSubscription({
+ getCurrentValue: React.useCallback(() => source.getValue(), [source]),
+ subscribe: React.useCallback(
+ callback => {
+ subscriptions.push(source);
+ const subscription = source.subscribe(callback);
+ return () => subscription.unsubscribe();
+ },
+ [source],
+ ),
+ });
+ return ;
+ }
+
+ const observableA = createBehaviorSubject('a-0');
+ const observableB = createBehaviorSubject('b-0');
+
+ expect(subscriptions).toHaveLength(0);
+
+ let renderer;
+ act(() => {
+ renderer = ReactTestRenderer.create(
+ ,
+ {unstable_isConcurrent: true},
+ );
+ });
+
+ // Updates while subscribed should re-render the child component
+ expect(Scheduler).toHaveYielded(['a-0']);
+ expect(subscriptions).toHaveLength(1);
+ expect(subscriptions[0]).toBe(observableA);
+
+ // Unsetting the subscriber prop should reset subscribed values
+ act(() => renderer.update());
+ expect(Scheduler).toHaveYielded(['b-0']);
+ expect(subscriptions).toHaveLength(2);
+ expect(subscriptions[1]).toBe(observableB);
+
+ // Updates to the old subscribable should not re-render the child component
+ act(() => observableA.next('a-1'));
+ expect(Scheduler).toFlushAndYield([]);
+
+ // Updates to the bew subscribable should re-render the child component
+ act(() => observableB.next('b-1'));
+ expect(Scheduler).toHaveYielded(['b-1']);
+
+ expect(subscriptions).toHaveLength(2);
+ });
+
+ it('should ignore values emitted by a new subscribable until the commit phase', () => {
+ const log = [];
+
+ function Grandchild({value}) {
+ Scheduler.unstable_yieldValue('Grandchild: ' + value);
+ return null;
+ }
+
+ function Child({value = 'default'}) {
+ Scheduler.unstable_yieldValue('Child: ' + value);
+ return ;
+ }
+
+ function Subscription({source}) {
+ const value = useSubscription(
+ React.useMemo(
+ () => ({
+ getCurrentValue: () => source.getValue(),
+ subscribe: callback => {
+ const subscription = source.subscribe(callback);
+ return () => subscription.unsubscribe();
+ },
+ }),
+ [source],
+ ),
+ );
+ return ;
+ }
+
+ class Parent extends React.Component {
+ state = {};
+
+ static getDerivedStateFromProps(nextProps, prevState) {
+ if (nextProps.observed !== prevState.observed) {
+ return {
+ observed: nextProps.observed,
+ };
+ }
+
+ return null;
+ }
+
+ componentDidMount() {
+ log.push('Parent.componentDidMount');
+ }
+
+ componentDidUpdate() {
+ log.push('Parent.componentDidUpdate');
+ }
+
+ render() {
+ return ;
+ }
+ }
+
+ const observableA = createBehaviorSubject('a-0');
+ const observableB = createBehaviorSubject('b-0');
+
+ let renderer;
+ act(() => {
+ renderer = ReactTestRenderer.create(, {
+ unstable_isConcurrent: true,
+ });
+ });
+ expect(Scheduler).toHaveYielded(['Child: a-0', 'Grandchild: a-0']);
+ expect(log).toEqual(['Parent.componentDidMount']);
+
+ // Start React update, but don't finish
+ act(() => {
+ renderer.update();
+ expect(Scheduler).toFlushAndYieldThrough(['Child: b-0']);
+ expect(log).toEqual(['Parent.componentDidMount']);
+
+ // Emit some updates from the uncommitted subscribable
+ observableB.next('b-1');
+ observableB.next('b-2');
+ observableB.next('b-3');
+ });
+
+ // Update again
+ act(() => renderer.update());
+
+ // Flush everything and ensure that the correct subscribable is used
+ // We expect the last emitted update to be rendered (because of the commit phase value check)
+ // But the intermediate ones should be ignored,
+ // And the final rendered output should be the higher-priority observable.
+ expect(Scheduler).toHaveYielded([
+ 'Grandchild: b-0',
+ 'Child: b-3',
+ 'Grandchild: b-3',
+ 'Child: a-0',
+ 'Grandchild: a-0',
+ ]);
+ expect(log).toEqual([
+ 'Parent.componentDidMount',
+ 'Parent.componentDidUpdate',
+ 'Parent.componentDidUpdate',
+ ]);
+ });
+
+ it('should not drop values emitted between updates', () => {
+ const log = [];
+
+ function Grandchild({value}) {
+ Scheduler.unstable_yieldValue('Grandchild: ' + value);
+ return null;
+ }
+
+ function Child({value = 'default'}) {
+ Scheduler.unstable_yieldValue('Child: ' + value);
+ return ;
+ }
+
+ function Subscription({source}) {
+ const value = useSubscription(
+ React.useMemo(
+ () => ({
+ getCurrentValue: () => source.getValue(),
+ subscribe: callback => {
+ const subscription = source.subscribe(callback);
+ return () => subscription.unsubscribe();
+ },
+ }),
+ [source],
+ ),
+ );
+ return ;
+ }
+
+ class Parent extends React.Component {
+ state = {};
+
+ static getDerivedStateFromProps(nextProps, prevState) {
+ if (nextProps.observed !== prevState.observed) {
+ return {
+ observed: nextProps.observed,
+ };
+ }
+
+ return null;
+ }
+
+ componentDidMount() {
+ log.push('Parent.componentDidMount:' + this.props.observed.value);
+ }
+
+ componentDidUpdate() {
+ log.push('Parent.componentDidUpdate:' + this.props.observed.value);
+ }
+
+ render() {
+ return ;
+ }
+ }
+
+ const observableA = createBehaviorSubject('a-0');
+ const observableB = createBehaviorSubject('b-0');
+
+ let renderer;
+ act(() => {
+ renderer = ReactTestRenderer.create(, {
+ unstable_isConcurrent: true,
+ });
+ });
+ expect(Scheduler).toHaveYielded(['Child: a-0', 'Grandchild: a-0']);
+ expect(log).toEqual(['Parent.componentDidMount:a-0']);
+ log.splice(0);
+
+ // Start React update, but don't finish
+ act(() => {
+ renderer.update();
+ expect(Scheduler).toFlushAndYieldThrough(['Child: b-0']);
+ expect(log).toEqual([]);
+
+ // Emit some updates from the old subscribable
+ observableA.next('a-1');
+ observableA.next('a-2');
+
+ // Update again
+ renderer.update();
+
+ // Flush everything and ensure that the correct subscribable is used
+ // We expect the new subscribable to finish rendering,
+ // But then the updated values from the old subscribable should be used.
+ expect(Scheduler).toFlushAndYield([
+ 'Grandchild: b-0',
+ 'Child: a-2',
+ 'Grandchild: a-2',
+ ]);
+ expect(log).toEqual([
+ 'Parent.componentDidUpdate:b-0',
+ 'Parent.componentDidUpdate:a-2',
+ ]);
+ });
+
+ // Updates from the new subscribable should be ignored.
+ log.splice(0);
+ act(() => observableB.next('b-1'));
+ expect(Scheduler).toFlushAndYield([]);
+ expect(log).toEqual([]);
+ });
+
+ it('should guard against updates that happen after unmounting', () => {
+ function Child({value = 'default'}) {
+ Scheduler.unstable_yieldValue(value);
+ return null;
+ }
+
+ function Subscription({source}) {
+ const value = useSubscription(
+ React.useMemo(
+ () => ({
+ getCurrentValue: () => source.getValue(),
+ subscribe: callback => {
+ return source.subscribe(callback);
+ },
+ }),
+ [source],
+ ),
+ );
+ return ;
+ }
+
+ const eventHandler = {
+ _callbacks: [],
+ _value: true,
+ change(value) {
+ eventHandler._value = value;
+ const _callbacks = eventHandler._callbacks.slice(0);
+ _callbacks.forEach(callback => callback(value));
+ },
+ getValue() {
+ return eventHandler._value;
+ },
+ subscribe(callback) {
+ eventHandler._callbacks.push(callback);
+ return () => {
+ eventHandler._callbacks.splice(
+ eventHandler._callbacks.indexOf(callback),
+ 1,
+ );
+ };
+ },
+ };
+
+ eventHandler.subscribe(value => {
+ if (value === false) {
+ renderer.unmount();
+ expect(Scheduler).toFlushAndYield([]);
+ }
+ });
+
+ let renderer;
+ act(() => {
+ renderer = ReactTestRenderer.create(
+ ,
+ {unstable_isConcurrent: true},
+ );
+ });
+ expect(Scheduler).toHaveYielded([true]);
+
+ // This event should unmount
+ eventHandler.change(false);
+ });
+
+ it('does not return a value from the previous subscription if the source is updated', () => {
+ const subscription1 = {
+ getCurrentValue: () => 'one',
+ subscribe: () => () => {},
+ };
+
+ const subscription2 = {
+ getCurrentValue: () => 'two',
+ subscribe: () => () => {},
+ };
+
+ function Subscription({subscription}) {
+ const value = useSubscription(subscription);
+ if (value !== subscription.getCurrentValue()) {
+ throw Error(
+ `expected value "${subscription.getCurrentValue()}" but got value "${value}"`,
+ );
+ }
+ return null;
+ }
+
+ let renderer;
+ act(() => {
+ renderer = ReactTestRenderer.create(
+ ,
+ {unstable_isConcurrent: true},
+ );
+ });
+ Scheduler.unstable_flushAll();
+
+ act(() => renderer.update());
+ Scheduler.unstable_flushAll();
+ });
+});
diff --git a/packages/use-subscription/src/useSubscription.js b/packages/use-subscription/src/useSubscription.js
new file mode 100644
index 0000000000000..1bb80e2d7b131
--- /dev/null
+++ b/packages/use-subscription/src/useSubscription.js
@@ -0,0 +1,123 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import {useDebugValue, useEffect, useState} from 'react';
+
+// Hook used for safely managing subscriptions in concurrent mode.
+//
+// In order to avoid removing and re-adding subscriptions each time this hook is called,
+// the parameters passed to this hook should be memoized in some way–
+// either by wrapping the entire params object with useMemo()
+// or by wrapping the individual callbacks with useCallback().
+export function useSubscription({
+ // (Synchronously) returns the current value of our subscription.
+ getCurrentValue,
+
+ // This function is passed an event handler to attach to the subscription.
+ // It should return an unsubscribe function that removes the handler.
+ subscribe,
+}: {|
+ getCurrentValue: () => Value,
+ subscribe: (callback: Function) => () => void,
+|}): Value {
+ // Read the current value from our subscription.
+ // When this value changes, we'll schedule an update with React.
+ // It's important to also store the hook params so that we can check for staleness.
+ // (See the comment in checkForUpdates() below for more info.)
+ const [state, setState] = useState(() => ({
+ getCurrentValue,
+ subscribe,
+ value: getCurrentValue(),
+ }));
+
+ let valueToReturn = state.value;
+
+ // If parameters have changed since our last render, schedule an update with its current value.
+ if (
+ state.getCurrentValue !== getCurrentValue ||
+ state.subscribe !== subscribe
+ ) {
+ // If the subscription has been updated, we'll schedule another update with React.
+ // React will process this update immediately, so the old subscription value won't be committed.
+ // It is still nice to avoid returning a mismatched value though, so let's override the return value.
+ valueToReturn = getCurrentValue();
+
+ setState({
+ getCurrentValue,
+ subscribe,
+ value: valueToReturn,
+ });
+ }
+
+ // Display the current value for this hook in React DevTools.
+ useDebugValue(valueToReturn);
+
+ // It is important not to subscribe while rendering because this can lead to memory leaks.
+ // (Learn more at reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects)
+ // Instead, we wait until the commit phase to attach our handler.
+ //
+ // We intentionally use a passive effect (useEffect) rather than a synchronous one (useLayoutEffect)
+ // so that we don't stretch the commit phase.
+ // This also has an added benefit when multiple components are subscribed to the same source:
+ // It allows each of the event handlers to safely schedule work without potentially removing an another handler.
+ // (Learn more at https://codesandbox.io/s/k0yvr5970o)
+ useEffect(
+ () => {
+ let didUnsubscribe = false;
+
+ const checkForUpdates = () => {
+ // It's possible that this callback will be invoked even after being unsubscribed,
+ // if it's removed as a result of a subscription event/update.
+ // In this case, React will log a DEV warning about an update from an unmounted component.
+ // We can avoid triggering that warning with this check.
+ if (didUnsubscribe) {
+ return;
+ }
+
+ setState(prevState => {
+ // Ignore values from stale sources!
+ // Since we subscribe an unsubscribe in a passive effect,
+ // it's possible that this callback will be invoked for a stale (previous) subscription.
+ // This check avoids scheduling an update for that stale subscription.
+ if (
+ prevState.getCurrentValue !== getCurrentValue ||
+ prevState.subscribe !== subscribe
+ ) {
+ return prevState;
+ }
+
+ // Some subscriptions will auto-invoke the handler, even if the value hasn't changed.
+ // If the value hasn't changed, no update is needed.
+ // Return state as-is so React can bail out and avoid an unnecessary render.
+ const value = getCurrentValue();
+ if (prevState.value === value) {
+ return prevState;
+ }
+
+ return {...prevState, value};
+ });
+ };
+ const unsubscribe = subscribe(checkForUpdates);
+
+ // Because we're subscribing in a passive effect,
+ // it's possible that an update has occurred between render and our effect handler.
+ // Check for this and schedule an update if work has occurred.
+ checkForUpdates();
+
+ return () => {
+ didUnsubscribe = true;
+ unsubscribe();
+ };
+ },
+ [getCurrentValue, subscribe],
+ );
+
+ // Return the current value for our caller to use while rendering.
+ return valueToReturn;
+}