diff --git a/packages/react-hooks/src/__tests__/useSubscription-test.internal.js b/packages/react-hooks/src/__tests__/useSubscription-test.internal.js
index 3027a6c4c446..6f3ed4225d1c 100644
--- a/packages/react-hooks/src/__tests__/useSubscription-test.internal.js
+++ b/packages/react-hooks/src/__tests__/useSubscription-test.internal.js
@@ -49,34 +49,29 @@ describe('useSubscription', () => {
return replaySubject;
}
- // Mimic createSubscription API to simplify testing
- function createSubscription({getCurrentValue, subscribe}) {
- return function Subscription({children, source}) {
+ it('supports basic subscription pattern', () => {
+ function Subscription({source}) {
const value = useSubscription(
- React.useMemo(() => ({source, getCurrentValue, subscribe}), [source]),
+ () => ({
+ getCurrentValue: () => source.getValue(),
+ subscribe: callback => {
+ const subscription = source.subscribe(callback);
+ return () => subscription.unsubscribe();
+ },
+ }),
+ [source],
);
+ return ;
+ }
- return React.createElement(children, {value});
- };
- }
-
- it('supports basic subscription pattern', () => {
- const Subscription = createSubscription({
- getCurrentValue: source => source.getValue(),
- subscribe: (source, callback) => {
- const subscription = source.subscribe(callback);
- return () => subscription.unsubscribe();
- },
- });
+ function Child({value = 'default'}) {
+ Scheduler.yieldValue(value);
+ return null;
+ }
const observable = createBehaviorSubject();
const renderer = ReactTestRenderer.create(
-
- {({value = 'default'}) => {
- Scheduler.yieldValue(value);
- return null;
- }}
- ,
+ ,
{unstable_isConcurrent: true},
);
@@ -94,30 +89,36 @@ describe('useSubscription', () => {
});
it('should support observable types like RxJS ReplaySubject', () => {
- const Subscription = createSubscription({
- getCurrentValue: source => {
- let currentValue;
- source
- .subscribe(value => {
- currentValue = value;
- })
- .unsubscribe();
- return currentValue;
- },
- subscribe: (source, callback) => {
- const subscription = source.subscribe(callback);
- return () => subscription.unsubscribe;
- },
- });
+ function Subscription({source}) {
+ const value = useSubscription(
+ () => ({
+ getCurrentValue: () => {
+ let currentValue;
+ source
+ .subscribe(tempValue => {
+ currentValue = tempValue;
+ })
+ .unsubscribe();
+ return currentValue;
+ },
+ subscribe: callback => {
+ const subscription = source.subscribe(callback);
+ return () => subscription.unsubscribe();
+ },
+ }),
+ [source],
+ );
+ return ;
+ }
- function render({value = 'default'}) {
+ function Child({value = 'default'}) {
Scheduler.yieldValue(value);
return null;
}
let observable = createReplaySubject('initial');
const renderer = ReactTestRenderer.create(
- {render},
+ ,
{unstable_isConcurrent: true},
);
expect(Scheduler).toFlushAndYield(['initial']);
@@ -128,20 +129,26 @@ describe('useSubscription', () => {
// Unsetting the subscriber prop should reset subscribed values
observable = createReplaySubject(undefined);
- renderer.update({render});
+ renderer.update();
expect(Scheduler).toFlushAndYield(['default']);
});
it('should unsubscribe from old subscribables and subscribe to new subscribables when props change', () => {
- const Subscription = createSubscription({
- getCurrentValue: source => source.getValue(),
- subscribe: (source, callback) => {
- const subscription = source.subscribe(callback);
- return () => subscription.unsubscribe();
- },
- });
+ function Subscription({source}) {
+ const value = useSubscription(
+ () => ({
+ getCurrentValue: () => source.getValue(),
+ subscribe: callback => {
+ const subscription = source.subscribe(callback);
+ return () => subscription.unsubscribe();
+ },
+ }),
+ [source],
+ );
+ return ;
+ }
- function render({value = 'default'}) {
+ function Child({value = 'default'}) {
Scheduler.yieldValue(value);
return null;
}
@@ -150,7 +157,7 @@ describe('useSubscription', () => {
const observableB = createBehaviorSubject('b-0');
const renderer = ReactTestRenderer.create(
- {render},
+ ,
{unstable_isConcurrent: true},
);
@@ -158,7 +165,7 @@ describe('useSubscription', () => {
expect(Scheduler).toFlushAndYield(['a-0']);
// Unsetting the subscriber prop should reset subscribed values
- renderer.update({render});
+ renderer.update();
expect(Scheduler).toFlushAndYield(['b-0']);
// Updates to the old subscribable should not re-render the child component
@@ -173,32 +180,31 @@ describe('useSubscription', () => {
it('should ignore values emitted by a new subscribable until the commit phase', () => {
const log = [];
- function Child({value}) {
- Scheduler.yieldValue('Child: ' + value);
- return null;
+ function Subscription({source}) {
+ const value = useSubscription(
+ () => ({
+ getCurrentValue: () => source.getValue(),
+ subscribe: callback => {
+ const subscription = source.subscribe(callback);
+ return () => subscription.unsubscribe();
+ },
+ }),
+ [source],
+ );
+ return ;
}
- const Subscription = createSubscription({
- getCurrentValue: source => source.getValue(),
- subscribe: (source, callback) => {
- const subscription = source.subscribe(callback);
- return () => subscription.unsubscribe();
- },
- });
-
- class Parent extends React.Component {
- state = {};
-
- static getDerivedStateFromProps(nextProps, prevState) {
- if (nextProps.observed !== prevState.observed) {
- return {
- observed: nextProps.observed,
- };
- }
+ function Outer({value}) {
+ Scheduler.yieldValue('Outer: ' + value);
+ return ;
+ }
- return null;
- }
+ function Inner({value}) {
+ Scheduler.yieldValue('Inner: ' + value);
+ return null;
+ }
+ class Parent extends React.Component {
componentDidMount() {
log.push('Parent.componentDidMount');
}
@@ -208,14 +214,7 @@ describe('useSubscription', () => {
}
render() {
- return (
-
- {({value = 'default'}) => {
- Scheduler.yieldValue('Subscriber: ' + value);
- return ;
- }}
-
- );
+ return ;
}
}
@@ -226,12 +225,12 @@ describe('useSubscription', () => {
,
{unstable_isConcurrent: true},
);
- expect(Scheduler).toFlushAndYield(['Subscriber: a-0', 'Child: a-0']);
+ expect(Scheduler).toFlushAndYield(['Outer: a-0', 'Inner: a-0']);
expect(log).toEqual(['Parent.componentDidMount']);
// Start React update, but don't finish
renderer.update();
- expect(Scheduler).toFlushAndYieldThrough(['Subscriber: b-0']);
+ expect(Scheduler).toFlushAndYieldThrough(['Outer: b-0']);
expect(log).toEqual(['Parent.componentDidMount']);
// Emit some updates from the uncommitted subscribable
@@ -247,11 +246,11 @@ describe('useSubscription', () => {
// But the intermediate ones should be ignored,
// And the final rendered output should be the higher-priority observable.
expect(Scheduler).toFlushAndYield([
- 'Child: b-0',
- 'Subscriber: b-3',
- 'Child: b-3',
- 'Subscriber: a-0',
- 'Child: a-0',
+ 'Inner: b-0',
+ 'Outer: b-3',
+ 'Inner: b-3',
+ 'Outer: a-0',
+ 'Inner: a-0',
]);
expect(log).toEqual([
'Parent.componentDidMount',
@@ -263,32 +262,31 @@ describe('useSubscription', () => {
it('should not drop values emitted between updates', () => {
const log = [];
- function Child({value}) {
- Scheduler.yieldValue('Child: ' + value);
- return null;
+ function Subscription({source}) {
+ const value = useSubscription(
+ () => ({
+ getCurrentValue: () => source.getValue(),
+ subscribe: callback => {
+ const subscription = source.subscribe(callback);
+ return () => subscription.unsubscribe();
+ },
+ }),
+ [source],
+ );
+ return ;
}
- const Subscription = createSubscription({
- getCurrentValue: source => source.getValue(),
- subscribe: (source, callback) => {
- const subscription = source.subscribe(callback);
- return () => subscription.unsubscribe();
- },
- });
-
- class Parent extends React.Component {
- state = {};
-
- static getDerivedStateFromProps(nextProps, prevState) {
- if (nextProps.observed !== prevState.observed) {
- return {
- observed: nextProps.observed,
- };
- }
+ function Outer({value}) {
+ Scheduler.yieldValue('Outer: ' + value);
+ return ;
+ }
- return null;
- }
+ function Inner({value}) {
+ Scheduler.yieldValue('Inner: ' + value);
+ return null;
+ }
+ class Parent extends React.Component {
componentDidMount() {
log.push('Parent.componentDidMount');
}
@@ -298,14 +296,7 @@ describe('useSubscription', () => {
}
render() {
- return (
-
- {({value = 'default'}) => {
- Scheduler.yieldValue('Subscriber: ' + value);
- return ;
- }}
-
- );
+ return ;
}
}
@@ -316,12 +307,12 @@ describe('useSubscription', () => {
,
{unstable_isConcurrent: true},
);
- expect(Scheduler).toFlushAndYield(['Subscriber: a-0', 'Child: a-0']);
+ expect(Scheduler).toFlushAndYield(['Outer: a-0', 'Inner: a-0']);
expect(log).toEqual(['Parent.componentDidMount']);
// Start React update, but don't finish
renderer.update();
- expect(Scheduler).toFlushAndYieldThrough(['Subscriber: b-0']);
+ expect(Scheduler).toFlushAndYieldThrough(['Outer: b-0']);
expect(log).toEqual(['Parent.componentDidMount']);
// Emit some updates from the old subscribable
@@ -335,9 +326,9 @@ describe('useSubscription', () => {
// We expect the new subscribable to finish rendering,
// But then the updated values from the old subscribable should be used.
expect(Scheduler).toFlushAndYield([
- 'Child: b-0',
- 'Subscriber: a-2',
- 'Child: a-2',
+ 'Inner: b-0',
+ 'Outer: a-2',
+ 'Inner: a-2',
]);
expect(log).toEqual([
'Parent.componentDidMount',
@@ -356,12 +347,24 @@ describe('useSubscription', () => {
});
it('should guard against updates that happen after unmounting', () => {
- const Subscription = createSubscription({
- getCurrentValue: source => source.getValue(),
- subscribe: (source, callback) => {
- return source.subscribe(callback);
- },
- });
+ function Subscription({source}) {
+ const value = useSubscription(
+ () => ({
+ getCurrentValue: () => source.getValue(),
+ subscribe: callback => {
+ const unsubscribe = source.subscribe(callback);
+ return () => unsubscribe();
+ },
+ }),
+ [source],
+ );
+ return ;
+ }
+
+ function Child({value}) {
+ Scheduler.yieldValue(value);
+ return null;
+ }
const eventHandler = {
_callbacks: [],
@@ -393,12 +396,7 @@ describe('useSubscription', () => {
});
const renderer = ReactTestRenderer.create(
-
- {({value}) => {
- Scheduler.yieldValue(value);
- return null;
- }}
- ,
+ ,
{unstable_isConcurrent: true},
);
diff --git a/packages/react-hooks/src/useSubscription.js b/packages/react-hooks/src/useSubscription.js
index ee4078fe2c4c..c749203cbaa8 100644
--- a/packages/react-hooks/src/useSubscription.js
+++ b/packages/react-hooks/src/useSubscription.js
@@ -1,30 +1,49 @@
-import {useEffect, useState} from 'react';
+/**
+ * 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
+ */
-export function useSubscription({
- // This is the thing being subscribed to (e.g. an observable, event dispatcher, etc).
- source,
+import {useEffect, useMemo, useState} from 'react';
- // (Synchronously) returns the current value of our subscription source.
- getCurrentValue,
+// Hook used for safely managing subscriptions in concurrent mode.
+// It requires two parameters: a factory function and a dependencies array.
+export function useSubscription(
+ // This function is called whenever the specified dependencies change.
+ // It should return an object with two keys (functions) documented below.
+ nextCreate: () => {|
+ // Get the current subscription value.
+ getCurrentValue: () => T,
- // This function is passed an event handler to attach to the subscription source.
- // It should return an unsubscribe function that removes the handler.
- subscribe,
-}) {
- // Read the current value from our subscription source.
+ // This function is passed a callback to be called any time the subscription changes.
+ // It should return an unsubscribe function.
+ subscribe: (() => void) => () => void,
+ |},
+ // Dependencies array.
+ // Any time one of the inputs change, a new subscription will be setup,
+ // and the previous listener will be unsubscribed.
+ deps: Array,
+) {
+ const current = useMemo(nextCreate, deps);
+
+ // Read the current subscription value.
// When this value changes, we'll schedule an update with React.
- // It's important to also store the source itself so that we can check for staleness.
+ // It's important to also store the current inputs as well so we can check for staleness.
// (See the comment in checkForUpdates() below for more info.)
const [state, setState] = useState({
- source,
- value: getCurrentValue(source),
+ current,
+ value: current.getCurrentValue(),
});
- // If the source has changed since our last render, schedule an update with its current value.
- if (state.source !== source) {
+ // If the inputs have changed since our last render, schedule an update with the current value.
+ // We could do this in our effect handler below but there's no need to wait in this case.
+ if (state.current !== current) {
setState({
- source,
- value: getCurrentValue(source),
+ current,
+ value: current.getCurrentValue(),
});
}
@@ -51,18 +70,18 @@ export function useSubscription({
}
setState(prevState => {
- // Ignore values from stale sources!
+ // Ignore values from stale subscriptions!
// Since we subscribe an unsubscribe in a passive effect,
- // it's possible that this callback will be invoked for a stale (previous) source.
- // This check avoids scheduling an update for htat stale source.
- if (prevState.source !== source) {
+ // it's possible that this callback will be invoked for a stale (previous) subscription.
+ // This check avoids scheduling an update for the stale subscription.
+ if (prevState.current !== current) {
return prevState;
}
- // Some subscription sources will auto-invoke the handler, even if the value hasn't changed.
+ // Some subscriptions will auto-invoke the handler when it's attached.
// 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(source);
+ const value = current.getCurrentValue();
if (prevState.value === value) {
return prevState;
}
@@ -70,7 +89,8 @@ export function useSubscription({
return {...prevState, value};
});
};
- const unsubscribe = subscribe(source, checkForUpdates);
+
+ const unsubscribe = current.subscribe(checkForUpdates);
// Because we're subscribing in a passive effect,
// it's possible that an update has occurred between render and our effect handler.
@@ -82,7 +102,7 @@ export function useSubscription({
unsubscribe();
};
},
- [getCurrentValue, source, subscribe],
+ [current],
);
// Return the current value for our caller to use while rendering.