277 lines
7.5 KiB
Plaintext
277 lines
7.5 KiB
Plaintext
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*
|
|
* @providesModule FluxContainer
|
|
* @flow
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
import type FluxStore from 'FluxStore';
|
|
|
|
const FluxContainerSubscriptions = require('FluxContainerSubscriptions');
|
|
const React = require('react');
|
|
|
|
const invariant = require('invariant');
|
|
const shallowEqual = require('shallowEqual');
|
|
|
|
const {Component} = React;
|
|
|
|
type Options = {
|
|
pure?: ?boolean,
|
|
withProps?: ?boolean,
|
|
withContext?: ?boolean,
|
|
};
|
|
|
|
const DEFAULT_OPTIONS = {
|
|
pure: true,
|
|
withProps: false,
|
|
withContext: false,
|
|
};
|
|
|
|
/**
|
|
* A FluxContainer is used to subscribe a react component to multiple stores.
|
|
* The stores that are used must be returned from a static `getStores()` method.
|
|
*
|
|
* The component receives information from the stores via state. The state
|
|
* is generated using a static `calculateState()` method that each container
|
|
* must implement. A simple container may look like:
|
|
*
|
|
* class FooContainer extends Component {
|
|
* static getStores() {
|
|
* return [FooStore];
|
|
* }
|
|
*
|
|
* static calculateState() {
|
|
* return {
|
|
* foo: FooStore.getState(),
|
|
* };
|
|
* }
|
|
*
|
|
* render() {
|
|
* return <FooView {...this.state} />;
|
|
* }
|
|
* }
|
|
*
|
|
* module.exports = FluxContainer.create(FooContainer);
|
|
*
|
|
* Flux container also supports some other, more advanced use cases. If you need
|
|
* to base your state off of props as well:
|
|
*
|
|
* class FooContainer extends Component {
|
|
* ...
|
|
*
|
|
* static calculateState(prevState, props) {
|
|
* return {
|
|
* foo: FooStore.getSpecificFoo(props.id),
|
|
* };
|
|
* }
|
|
*
|
|
* ...
|
|
* }
|
|
*
|
|
* module.exports = FluxContainer.create(FooContainer, {withProps: true});
|
|
*
|
|
* Or if your stores are passed through your props:
|
|
*
|
|
* class FooContainer extends Component {
|
|
* ...
|
|
*
|
|
* static getStores(props) {
|
|
* const {BarStore, FooStore} = props.stores;
|
|
* return [BarStore, FooStore];
|
|
* }
|
|
*
|
|
* static calculateState(prevState, props) {
|
|
* const {BarStore, FooStore} = props.stores;
|
|
* return {
|
|
* bar: BarStore.getState(),
|
|
* foo: FooStore.getState(),
|
|
* };
|
|
* }
|
|
*
|
|
* ...
|
|
* }
|
|
*
|
|
* module.exports = FluxContainer.create(FooContainer, {withProps: true});
|
|
*/
|
|
function create<DefaultProps, Props, State>(
|
|
Base: ReactClass<Props>,
|
|
options?: ?Options,
|
|
): ReactClass<Props> {
|
|
enforceInterface(Base);
|
|
|
|
// Construct the options using default, override with user values as necessary.
|
|
const realOptions = {
|
|
...DEFAULT_OPTIONS,
|
|
...(options || {}),
|
|
};
|
|
|
|
const getState = (state, maybeProps, maybeContext) => {
|
|
const props = realOptions.withProps ? maybeProps : undefined;
|
|
const context = realOptions.withContext ? maybeContext : undefined;
|
|
return Base.calculateState(state, props, context);
|
|
};
|
|
|
|
const getStores = (maybeProps, maybeContext) => {
|
|
const props = realOptions.withProps ? maybeProps : undefined;
|
|
const context = realOptions.withContext ? maybeContext : undefined;
|
|
return Base.getStores(props, context);
|
|
};
|
|
|
|
// Build the container class.
|
|
class ContainerClass extends Base {
|
|
_fluxContainerSubscriptions: FluxContainerSubscriptions;
|
|
|
|
constructor(props: Props, context: any) {
|
|
super(props, context);
|
|
this._fluxContainerSubscriptions = new FluxContainerSubscriptions();
|
|
this._fluxContainerSubscriptions.setStores(getStores(props, context));
|
|
this._fluxContainerSubscriptions.addListener(() => {
|
|
this.setState((prevState, currentProps) =>
|
|
getState(prevState, currentProps, context),
|
|
);
|
|
});
|
|
const calculatedState = getState(undefined, props, context);
|
|
this.state = {
|
|
...(this.state || {}),
|
|
...calculatedState,
|
|
};
|
|
}
|
|
|
|
UNSAFE_componentWillReceiveProps(nextProps: any, nextContext: any): void {
|
|
if (super.UNSAFE_componentWillReceiveProps) {
|
|
super.UNSAFE_componentWillReceiveProps(nextProps, nextContext);
|
|
}
|
|
|
|
if (super.componentWillReceiveProps) {
|
|
super.componentWillReceiveProps(nextProps, nextContext);
|
|
}
|
|
|
|
if (realOptions.withProps || realOptions.withContext) {
|
|
// Update both stores and state.
|
|
this._fluxContainerSubscriptions.setStores(
|
|
getStores(nextProps, nextContext),
|
|
);
|
|
this.setState((prevState) =>
|
|
getState(prevState, nextProps, nextContext),
|
|
);
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
if (super.componentWillUnmount) {
|
|
super.componentWillUnmount();
|
|
}
|
|
|
|
this._fluxContainerSubscriptions.reset();
|
|
}
|
|
}
|
|
|
|
// Make sure we override shouldComponentUpdate only if the pure option is
|
|
// specified. We can't override this above because we don't want to override
|
|
// the default behavior on accident. Super works weird with react ES6 classes.
|
|
const container = realOptions.pure
|
|
? createPureComponent(ContainerClass)
|
|
: ContainerClass;
|
|
|
|
// Update the name of the container before returning
|
|
const componentName = Base.displayName || Base.name;
|
|
container.displayName = 'FluxContainer(' + componentName + ')';
|
|
return container;
|
|
}
|
|
|
|
function createPureComponent<DefaultProps, Props, State>(
|
|
BaseComponent: ReactClass<Props>,
|
|
): ReactClass<Props> {
|
|
class PureComponent extends BaseComponent {
|
|
shouldComponentUpdate(nextProps: Props, nextState: State): boolean {
|
|
return (
|
|
!shallowEqual(this.props, nextProps) ||
|
|
!shallowEqual(this.state, nextState)
|
|
);
|
|
}
|
|
}
|
|
return PureComponent;
|
|
}
|
|
|
|
function enforceInterface(o: any): void {
|
|
invariant(
|
|
o.getStores,
|
|
'Components that use FluxContainer must implement `static getStores()`',
|
|
);
|
|
invariant(
|
|
o.calculateState,
|
|
'Components that use FluxContainer must implement `static calculateState()`',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* This is a way to connect stores to a functional stateless view. Here's a
|
|
* simple example:
|
|
*
|
|
* // FooView.js
|
|
*
|
|
* function FooView(props) {
|
|
* return <div>{props.value}</div>;
|
|
* }
|
|
*
|
|
* module.exports = FooView;
|
|
*
|
|
*
|
|
* // FooContainer.js
|
|
*
|
|
* function getStores() {
|
|
* return [FooStore];
|
|
* }
|
|
*
|
|
* function calculateState() {
|
|
* return {
|
|
* value: FooStore.getState();
|
|
* };
|
|
* }
|
|
*
|
|
* module.exports = FluxContainer.createFunctional(
|
|
* FooView,
|
|
* getStores,
|
|
* calculateState,
|
|
* );
|
|
*
|
|
*/
|
|
function createFunctional<Props, State, A, B>(
|
|
viewFn: (props: State) => React.Element<State>,
|
|
getStores: (props?: ?Props, context?: any) => Array<FluxStore>,
|
|
calculateState: (prevState?: ?State, props?: ?Props, context?: any) => State,
|
|
options?: Options,
|
|
): ReactClass<Props> {
|
|
class FunctionalContainer extends Component<void, Props, State> {
|
|
state: State;
|
|
static getStores(props?: ?Props, context?: any): Array<FluxStore> {
|
|
return getStores(props, context);
|
|
}
|
|
|
|
static calculateState(
|
|
prevState?: ?State,
|
|
props?: ?Props,
|
|
context?: any,
|
|
): State {
|
|
return calculateState(prevState, props, context);
|
|
}
|
|
|
|
render(): React.Element<State> {
|
|
return viewFn(this.state);
|
|
}
|
|
}
|
|
// Update the name of the component before creating the container.
|
|
const viewFnName = viewFn.displayName || viewFn.name || 'FunctionalContainer';
|
|
FunctionalContainer.displayName = viewFnName;
|
|
return create(FunctionalContainer, options);
|
|
}
|
|
|
|
module.exports = {create, createFunctional};
|