Skip to content

Instantly share code, notes, and snippets.

@nealeu
Last active November 8, 2018 01:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nealeu/23ee4919fab5ad0a776d1d741bb556a5 to your computer and use it in GitHub Desktop.
Save nealeu/23ee4919fab5ad0a776d1d741bb556a5 to your computer and use it in GitHub Desktop.
Typescript typings for react-async
declare module "react-async" {
import {ComponentType, ReactNode} from "react";
import * as React from "react";
export interface LoadingProps<V> {
/** Show only on initial load (data is undefined) */
initial?: boolean;
/** Function (passing state) or React node */
children: ReactNode | ((state: State<V>) => ReactNode);
}
export interface PendingProps<V> {
/** Show old data while loading */
persist?: boolean;
children: ReactNode | ((data: V) => ReactNode);
}
export interface RejectedProps<V> {
/** Show old error while loading */
persist?: boolean;
/** Function (passing error and state) or React node */
children: ReactNode | ((error: Error, state: State<V>) => ReactNode);
}
/** Can extend this for specifying a functional component
* function MyThingWithDefaultLoadAndError(props: {param:number} & ResolvedProps<V>)
*/
export interface ResolvedProps<V> {
/** Show old data while loading */
persist?: boolean;
/** Function (passing data and state) or React node */
children: ReactNode | ((data: V, state: State<V>) => ReactNode);
}
type Resolved<T> = ComponentType<ResolvedProps<T>>;
/** A type to have args for promiseFn also be part of component props */
type AsyncProps<V, ARGS> = Props<V, ARGS> & ARGS;
interface Props<V, ARGS> {
initialValue?: V | Error;
promiseFn?: (props: AsyncProps<V, ARGS>) => Promise<V>;
deferFn?: (...args: any[]) => Promise<V>;
onResolve?: (args) => unknown;
onReject?: (error: Error) => unknown;
watch?: any; // compared using !== to see if this has changed to determine if promiseFn should be re-evaluated
children?: ReactNode | Element | Element[] | ((value: State<V>) => ReactNode);
}
interface State<V> {
initialValue: V | Error;
data?: V;
error?: Error;
isLoading: boolean;
startedAt?: Date;
finishedAt?: Date;
cancel: () => void;
run: () => void;
reload: () => void;
setData(data: V, callback: () => any): V;
setError(error: Error, callback: () => any): Error;
}
export type AsyncState<V> = State<V>;
// Write the interface of the constructor
export interface AsyncClass<V = any, A = any> extends React.ComponentClass<AsyncProps<V, A>> {
Pending: ComponentType<PendingProps<V>>;
Loading: ComponentType<LoadingProps<V>>;
Resolved: Resolved<V> & AsyncProps<V, A>;
Rejected: ComponentType<RejectedProps<V>>;
}
/**
* e.g.
* const MyAsyncClass: AsyncClass<MyData> = createInstance();
* const Element: AsyncElement<MyData> = <MyAsyncClass props... />;
*/
export interface AsyncElement<V, A> extends React.ReactElement<AsyncProps<V, A>> {
}
/** Creates a new Class instance with it's own private context for linking together
* Async, Loading, Resolved, Rejected via Context API.
* See https://stackoverflow.com/a/31815634/1998186 if we have issues of Class vs instance vs Element
*/
export function createInstance<V, A>(defaultProps?: AsyncProps<V, A>): AsyncClass<V, A>;
export const Async: AsyncClass;
export default Async; // Not sure if this or one above is right
}
import * as React from "react"
import {ReactNode} from "react";
const isFunction = arg => typeof arg === "function";
interface Props<V> {
initialValue?: V | Error;
promiseFn?: (props: Props<V>) => Promise<V>;
deferFn?: (...args: any[]) => Promise<V>;
onResolve?: (args) => unknown;
onReject?: (error: Error) => unknown;
watch?: any; // compared using !== to see if this has changed to determine if promiseFn should be re-evaluated
children?: (value: State<V>) => ReactNode;
}
interface State<V> {
initialValue: V | Error;
data?: V;
error?: Error;
isLoading: boolean;
startedAt?: Date;
finishedAt?: Date;
cancel: () => void;
run: () => void;
reload: () => void;
setData(data: V, callback: () => any): V;
setError(error: Error, callback: () => any): Error;
}
/**
* 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<I>(defaultProps: Props<any> = {}): React.ComponentClass<Props<I>> {
const { Consumer, Provider } = React.createContext({});
class Async<V> extends React.Component<Props<V>, State<V>> {
private mounted: boolean;
private counter: number;
private args: any[];
constructor(props: Props<V>) {
super(props);
const promiseFn = props.promiseFn || defaultProps.promiseFn;
const initialValue = props.initialValue || defaultProps.initialValue;
const initialError = initialValue instanceof Error ? initialValue : undefined;
const initialData = initialError ? undefined : initialValue;
this.mounted = false;
this.counter = 0;
this.args = [];
this.state = {
initialValue,
data: initialData,
error: initialError,
isLoading: !initialValue && isFunction(promiseFn),
startedAt: undefined,
finishedAt: initialValue ? new Date() : undefined,
cancel: this.cancel,
run: this.run,
reload: () => {
this.load();
this.run(...this.args)
},
setData: this.setData,
setError: this.setError
}
}
componentDidMount() {
this.mounted = true;
this.state.initialValue || this.load()
}
componentDidUpdate(prevProps) {
if (prevProps.watch !== this.props.watch) this.load();
if (prevProps.promiseFn !== this.props.promiseFn) {
this.cancel();
if (this.props.promiseFn) this.load()
}
}
componentWillUnmount() {
this.cancel();
this.mounted = false
}
load = () => {
const promiseFn = this.props.promiseFn || defaultProps.promiseFn;
if (!promiseFn) return;
this.counter++;
this.setState({isLoading: true, startedAt: new Date(), finishedAt: undefined});
return promiseFn(this.props).then(this.onResolve(this.counter), this.onReject(this.counter))
};
run = (...args) => {
const deferFn = this.props.deferFn || defaultProps.deferFn;
if (!deferFn) return;
this.counter++;
this.args = args;
this.setState({isLoading: true, startedAt: new Date(), finishedAt: undefined});
return deferFn(...args, this.props).then(this.onResolve(this.counter), this.onReject(this.counter))
};
cancel = () => {
this.counter++;
this.setState({isLoading: false, startedAt: undefined})
};
onResolve = counter => data => {
if (this.mounted && this.counter === counter) {
const onResolve = this.props.onResolve || defaultProps.onResolve;
this.setData(data, () => onResolve && onResolve(data))
}
return data
};
onReject = counter => error => {
if (this.mounted && this.counter === counter) {
const onReject = this.props.onReject || defaultProps.onReject;
this.setError(error, () => onReject && onReject(error))
}
return error
};
setData = (data, callback) => {
this.setState({data, error: undefined, isLoading: false, finishedAt: new Date()}, callback);
return data
};
setError = (error, callback) => {
this.setState({error, isLoading: false, finishedAt: new Date()}, callback);
return error
};
render() {
const {children} = this.props;
if (isFunction(children)) {
return <Provider value={this.state}>{children(this.state)}</Provider>
}
if (children !== undefined && children !== null) {
return <Provider value={this.state}>{children}</Provider>
}
return null
}
/**
* Renders only when deferred promise is pending (not yet run).
*
* @prop {boolean} persist Show until we have data, even while loading or when an error occurred
* @prop {Function|Node} children Function (passing state) or React node
*/
static Pending = ({children, persist}) => (
<Consumer>
{(state: State<I>) => {
if (state.data !== undefined) return null;
if (!persist && state.isLoading) return null;
if (!persist && state.error !== undefined) return null;
return isFunction(children) ? children(state) : children || null
}}
</Consumer>
);
/**
* Renders only while loading.
*
* @prop {boolean} initial Show only on initial load (data is undefined)
* @prop {Function|Node} children Function (passing state) or React node
*/
static Loading = ({children, initial}) => (
<Consumer>
{(state: State<I>) => {
if (!state.isLoading) return null;
if (initial && state.data !== undefined) return null;
return isFunction(children) ? children(state) : children || null
}}
</Consumer>
);
/**
* Renders only when promise is resolved.
*
* @prop {boolean} persist Show old data while loading
* @prop {Function|Node} children Function (passing data and state) or React node
*/
static Resolved = ({children, persist}) => (
<Consumer>
{(state: State<I>) => {
if (state.data === undefined) return null;
if (!persist && state.isLoading) return null;
if (!persist && state.error !== undefined) return null;
return isFunction(children) ? children(state.data, state) : children || null
}}
</Consumer>
);
/**
* Renders only when promise is rejected.
*
* @prop {boolean} persist Show old error while loading
* @prop {Function|Node} children Function (passing error and state) or React node
*/
static Rejected = ({children, persist}) => (
<Consumer>
{(state: State<I>) => {
if (state.error === undefined) return null;
if (state.isLoading && !persist) return null;
return isFunction(children) ? children(state.error, state) : children || null
}}
</Consumer>
);
}
return Async
}
export default createInstance();
@nealeu
Copy link
Author

nealeu commented Oct 29, 2018

I've started adding types to react-async as an effort to understand how I might add ambient types for this. It's certainly an interesting pattern to apply .d.ts to.

@nealeu
Copy link
Author

nealeu commented Nov 7, 2018

If anyone is willing to pick up the .d.ts and add it as @types/react-async via DefinitelyTyped, then do go ahead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment