UNPKG

react-async

Version:

React component for declarative promise resolution and data fetching

219 lines (218 loc) 9.7 kB
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();