Files
react/packages/create-subscription/src/createSubscription.js
T
Dan Abramov e706721490 Update Flow to 0.84 (#17805)
* Update Flow to 0.84

* Fix violations

* Use inexact object syntax in files from fbsource

* Fix warning extraction to use a modern parser

* Codemod inexact objects to new syntax

* Tighten types that can be exact

* Revert unintentional formatting changes from codemod
2020-01-09 14:50:44 +00:00

159 lines
4.5 KiB
JavaScript

/**
* 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 React from 'react';
import invariant from 'shared/invariant';
type Unsubscribe = () => void;
export function createSubscription<Property, Value>(
config: $ReadOnly<{|
// Synchronously gets the value for the subscribed property.
// Return undefined if the subscribable value is undefined,
// Or does not support synchronous reading (e.g. native Promise).
getCurrentValue: (source: Property) => Value | void,
// Setup a subscription for the subscribable value in props, and return an unsubscribe function.
// Return empty function if the property cannot be unsubscribed from (e.g. native Promises).
// Due to the variety of change event types, subscribers should provide their own handlers.
// Those handlers should not attempt to update state though;
// They should call the callback() instead when a subscription changes.
subscribe: (
source: Property,
callback: (value: Value | void) => void,
) => Unsubscribe,
|}>,
): React$ComponentType<{|
children: (value: Value | void) => React$Node,
source: Property,
|}> {
const {getCurrentValue, subscribe} = config;
if (__DEV__) {
if (typeof getCurrentValue !== 'function') {
console.error('Subscription must specify a getCurrentValue function');
}
if (typeof subscribe !== 'function') {
console.error('Subscription must specify a subscribe function');
}
}
type Props = {|
children: (value: Value) => React$Element<any>,
source: Property,
|};
type State = {|
source: Property,
value: Value | void,
|};
// Reference: https://gist.github.com/bvaughn/d569177d70b50b58bff69c3c4a5353f3
class Subscription extends React.Component<Props, State> {
state: State = {
source: this.props.source,
value:
this.props.source != null
? getCurrentValue(this.props.source)
: undefined,
};
_hasUnmounted: boolean = false;
_unsubscribe: Unsubscribe | null = null;
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.source !== prevState.source) {
return {
source: nextProps.source,
value:
nextProps.source != null
? getCurrentValue(nextProps.source)
: undefined,
};
}
return null;
}
componentDidMount() {
this.subscribe();
}
componentDidUpdate(prevProps, prevState) {
if (this.state.source !== prevState.source) {
this.unsubscribe();
this.subscribe();
}
}
componentWillUnmount() {
this.unsubscribe();
// Track mounted to avoid calling setState after unmounting
// For source like Promises that can't be unsubscribed from.
this._hasUnmounted = true;
}
render() {
return this.props.children(this.state.value);
}
subscribe() {
const {source} = this.state;
if (source != null) {
const callback = (value: Value | void) => {
if (this._hasUnmounted) {
return;
}
this.setState(state => {
// If the value is the same, skip the unnecessary state update.
if (value === state.value) {
return null;
}
// If this event belongs to an old or uncommitted data source, ignore it.
if (source !== state.source) {
return null;
}
return {value};
});
};
// Store the unsubscribe method for later (in case the subscribable prop changes).
const unsubscribe = subscribe(source, callback);
invariant(
typeof unsubscribe === 'function',
'A subscription must return an unsubscribe function.',
);
// It's safe to store unsubscribe on the instance because
// We only read or write that property during the "commit" phase.
this._unsubscribe = unsubscribe;
// External values could change between render and mount,
// In some cases it may be important to handle this case.
const value = getCurrentValue(this.props.source);
if (value !== this.state.value) {
this.setState({value});
}
}
}
unsubscribe() {
if (typeof this._unsubscribe === 'function') {
this._unsubscribe();
}
this._unsubscribe = null;
}
}
return Subscription;
}