react-async
Version:
React component for declarative promise resolution and data fetching
219 lines (218 loc) • 9.7 kB
JavaScript
import React from "react";
import globalScope, { MockAbortController } from "./globalScope";
import { IfInitial, IfPending, IfFulfilled, IfRejected, IfSettled } from "./helpers";
import propTypes from "./propTypes";
import { neverSettle, ActionTypes, init, dispatchMiddleware, reducer as asyncReducer, } from "./reducer";
class Async extends React.Component {
}
/**
* createInstance allows you to create instances of Async that are bound to a specific promise.
* A unique instance also uses its own React context for better nesting capability.
*/
export function createInstance(defaultOptions = {}, displayName = "Async") {
const { Consumer: UnguardedConsumer, Provider } = React.createContext(undefined);
function Consumer({ children }) {
return (React.createElement(UnguardedConsumer, null, value => {
if (!value) {
throw new Error("this component should only be used within an associated <Async> component!");
}
return children(value);
}));
}
class Async extends React.Component {
constructor(props) {
super(props);
this.mounted = false;
this.counter = 0;
this.args = [];
this.promise = neverSettle;
this.abortController = new MockAbortController();
this.start = this.start.bind(this);
this.load = this.load.bind(this);
this.run = this.run.bind(this);
this.cancel = this.cancel.bind(this);
this.onResolve = this.onResolve.bind(this);
this.onReject = this.onReject.bind(this);
this.setData = this.setData.bind(this);
this.setError = this.setError.bind(this);
const promise = props.promise;
const promiseFn = props.promiseFn || defaultOptions.promiseFn;
const initialValue = props.initialValue || defaultOptions.initialValue;
this.state = {
...init({ initialValue, promise, promiseFn }),
cancel: this.cancel,
run: this.run,
reload: () => {
this.load();
this.run(...this.args);
},
setData: this.setData,
setError: this.setError,
};
this.debugLabel = props.debugLabel || defaultOptions.debugLabel;
const { devToolsDispatcher } = globalScope.__REACT_ASYNC__;
const _reducer = props.reducer || defaultOptions.reducer;
const _dispatcher = props.dispatcher || defaultOptions.dispatcher || devToolsDispatcher;
const reducer = _reducer
? (state, action) => _reducer(state, action, asyncReducer)
: asyncReducer;
const dispatch = dispatchMiddleware((action, callback) => {
this.setState(state => reducer(state, action), callback);
});
this.dispatch = _dispatcher ? action => _dispatcher(action, dispatch, props) : dispatch;
}
componentDidMount() {
this.mounted = true;
if (this.props.promise || !this.state.initialValue) {
this.load();
}
}
componentDidUpdate(prevProps) {
const { watch, watchFn = defaultOptions.watchFn, promise, promiseFn } = this.props;
if (watch !== prevProps.watch) {
if (this.counter)
this.cancel();
return this.load();
}
if (watchFn &&
watchFn({ ...defaultOptions, ...this.props }, { ...defaultOptions, ...prevProps })) {
if (this.counter)
this.cancel();
return this.load();
}
if (promise !== prevProps.promise) {
if (this.counter)
this.cancel();
if (promise)
return this.load();
}
if (promiseFn !== prevProps.promiseFn) {
if (this.counter)
this.cancel();
if (promiseFn)
return this.load();
}
}
componentWillUnmount() {
this.cancel();
this.mounted = false;
}
getMeta(meta) {
return {
counter: this.counter,
promise: this.promise,
debugLabel: this.debugLabel,
...meta,
};
}
start(promiseFn) {
if ("AbortController" in globalScope) {
this.abortController.abort();
this.abortController = new globalScope.AbortController();
}
this.counter++;
return (this.promise = new Promise((resolve, reject) => {
if (!this.mounted)
return;
const executor = () => promiseFn().then(resolve, reject);
this.dispatch({ type: ActionTypes.start, payload: executor, meta: this.getMeta() });
}));
}
load() {
const promise = this.props.promise;
const promiseFn = this.props.promiseFn || defaultOptions.promiseFn;
if (promise) {
this.start(() => promise)
.then(this.onResolve(this.counter))
.catch(this.onReject(this.counter));
}
else if (promiseFn) {
const props = { ...defaultOptions, ...this.props };
this.start(() => promiseFn(props, this.abortController))
.then(this.onResolve(this.counter))
.catch(this.onReject(this.counter));
}
}
run(...args) {
const deferFn = this.props.deferFn || defaultOptions.deferFn;
if (deferFn) {
this.args = args;
const props = { ...defaultOptions, ...this.props };
return this.start(() => deferFn(args, props, this.abortController)).then(this.onResolve(this.counter), this.onReject(this.counter));
}
}
cancel() {
const onCancel = this.props.onCancel || defaultOptions.onCancel;
onCancel && onCancel();
this.counter++;
this.abortController.abort();
this.mounted && this.dispatch({ type: ActionTypes.cancel, meta: this.getMeta() });
}
onResolve(counter) {
return (data) => {
if (this.counter === counter) {
const onResolve = this.props.onResolve || defaultOptions.onResolve;
this.setData(data, () => onResolve && onResolve(data));
}
return data;
};
}
onReject(counter) {
return (error) => {
if (this.counter === counter) {
const onReject = this.props.onReject || defaultOptions.onReject;
this.setError(error, () => onReject && onReject(error));
}
return error;
};
}
setData(data, callback) {
this.mounted &&
this.dispatch({ type: ActionTypes.fulfill, payload: data, meta: this.getMeta() }, callback);
return data;
}
setError(error, callback) {
this.mounted &&
this.dispatch({ type: ActionTypes.reject, payload: error, error: true, meta: this.getMeta() }, callback);
return error;
}
render() {
const { children, suspense } = this.props;
if (suspense && this.state.isPending && this.promise !== neverSettle) {
// Rely on Suspense to handle the loading state
throw this.promise;
}
if (typeof children === "function") {
const render = children;
return React.createElement(Provider, { value: this.state }, render(this.state));
}
if (children !== undefined && children !== null) {
return React.createElement(Provider, { value: this.state }, children);
}
return null;
}
}
if (propTypes)
Async.propTypes = propTypes.Async;
const AsyncInitial = props => (React.createElement(Consumer, null, st => React.createElement(IfInitial, Object.assign({}, props, { state: st }))));
const AsyncPending = props => (React.createElement(Consumer, null, st => React.createElement(IfPending, Object.assign({}, props, { state: st }))));
const AsyncFulfilled = props => (React.createElement(Consumer, null, st => React.createElement(IfFulfilled, Object.assign({}, props, { state: st }))));
const AsyncRejected = props => (React.createElement(Consumer, null, st => React.createElement(IfRejected, Object.assign({}, props, { state: st }))));
const AsyncSettled = props => (React.createElement(Consumer, null, st => React.createElement(IfSettled, Object.assign({}, props, { state: st }))));
AsyncInitial.displayName = `${displayName}.Initial`;
AsyncPending.displayName = `${displayName}.Pending`;
AsyncFulfilled.displayName = `${displayName}.Fulfilled`;
AsyncRejected.displayName = `${displayName}.Rejected`;
AsyncSettled.displayName = `${displayName}.Settled`;
return Object.assign(Async, {
displayName: displayName,
Initial: AsyncInitial,
Pending: AsyncPending,
Loading: AsyncPending,
Fulfilled: AsyncFulfilled,
Resolved: AsyncFulfilled,
Rejected: AsyncRejected,
Settled: AsyncSettled,
});
}
export default createInstance();