-
-
Save brigand/8e5fef491924a31ac9b92623c9fe3bf3 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @flow | |
import * as React from 'react'; | |
import { Option } from 'safe-types'; | |
import usePromise from '~/hooks/usePromise'; | |
type State = 'initial' | 'pending' | 'resolved' | 'rejected'; | |
export class UsePromiseState<Result> { | |
_result: Result | null; | |
_error: mixed; | |
_state: State; | |
constructor(result: Result | null, error: any, state: State) { | |
this._result = result; | |
this._error = error; | |
this._state = state; | |
} | |
// Get the latest value... ignoring the state | |
value(): Option<Result> { | |
return Option.of(this._result); | |
} | |
// Get the latest error... ignoring the state | |
error(): Option<mixed> { | |
return Option.of(this._error); | |
} | |
// Get the value if in a success state | |
success(): Option<Result> { | |
return this.value().filter((x) => this._state === 'resolved'); | |
} | |
// Get the error if in an failure state | |
failure(): Option<mixed> { | |
return this.error().filter((x: mixed) => this._state === 'rejected'); | |
} | |
// Get if we're current in a loading state | |
loading(): boolean { | |
return this._state === 'pending'; | |
} | |
// Get if we're current in the initial state. | |
initial(): boolean { | |
return this._state === 'initial'; | |
} | |
// Get either Some(true) or None, for loading vs not loading | |
// Maybe this isn't needed because of match | |
loadingOption(): Option<true> { | |
if (this.loading()) { | |
return Option.Some(true); | |
} else { | |
return Option.None(); | |
} | |
} | |
match<Out>( | |
matcher: $Shape<{ | |
value: (value: Result) => Out, | |
error: (error: mixed) => Out, | |
success: (value: Result) => Out, | |
failure: (value: mixed) => Out, | |
loading: () => Out, | |
initial: () => Out, | |
default: () => Out, | |
}>, | |
): Out { | |
for (const key of Object.keys(matcher)) { | |
if (key === 'value' && matcher.value) { | |
const v = this.value(); | |
if (v.is_some()) { | |
return matcher.value(v.unwrap()); | |
} else { | |
continue; | |
} | |
} | |
if (key === 'error' && matcher.error) { | |
const v = this.error(); | |
if (v.is_some()) { | |
return matcher.error(v.unwrap()); | |
} else { | |
continue; | |
} | |
} | |
if (key === 'success' && matcher.success) { | |
const v = this.success(); | |
if (v.is_some()) { | |
return matcher.success(v.unwrap()); | |
} else { | |
continue; | |
} | |
} | |
if (key === 'failure' && matcher.failure) { | |
const v = this.failure(); | |
if (v.is_some()) { | |
return matcher.failure(v.unwrap()); | |
} else { | |
continue; | |
} | |
} | |
if (key === 'loading' && matcher.loading) { | |
if (this.loading()) { | |
return matcher.loading(); | |
} else { | |
continue; | |
} | |
} | |
if (key === 'initial' && matcher.initial) { | |
if (this.initial()) { | |
return matcher.initial(); | |
} else { | |
continue; | |
} | |
} | |
if (key !== 'default') { | |
throw new Error( | |
`Unexpected matcher key "${key}". See src/utils/usePromise for the expected API`, | |
); | |
} | |
} | |
if (matcher.default) { | |
return matcher.default(); | |
} | |
throw new Error( | |
`No cases matched. Define a 'default' case as a fallback if needed.`, | |
); | |
} | |
/// Allows matching on multiple states at once, and getting Option<Option<T>> where T is the success type. | |
/// Returns None if none of the provided variants match | |
/// Returns Some(Some(value)) if the success/value variants match | |
/// Returns Some(None) if any other variant matches | |
anyOf( | |
...variants: Array< | |
'failure' | 'success' | 'value' | 'error' | 'loading' | 'initial', | |
> | |
): Option<Option<Result>> { | |
return this.match({ | |
failure: () => | |
variants.includes('failure') ? Option.Some(Option.None()) : Option.None(), | |
success: (value) => | |
variants.includes('success') | |
? Option.Some(Option.Some(value)) | |
: Option.None(), | |
value: (value) => | |
variants.includes('value') ? Option.Some(Option.Some(value)) : Option.None(), | |
error: () => | |
variants.includes('error') ? Option.Some(Option.None()) : Option.None(), | |
loading: () => | |
variants.includes('loading') ? Option.Some(Option.None()) : Option.None(), | |
initial: () => | |
variants.includes('initial') ? Option.Some(Option.None()) : Option.None(), | |
default: () => Option.None(), | |
}); | |
} | |
} | |
class Skip {} | |
export type Service = { | |
skip: () => Skip, | |
}; | |
type InputFunc<Result> = ( | |
service: Service, | |
) => Promise<Result> | Skip | Option<Promise<Result>>; | |
export type Input<Result> = | |
| Promise<Result> | |
| InputFunc<Result> | |
| Option<Promise<Result>>; | |
export type ShouldRun = () => boolean; | |
const alwaysRun: ShouldRun = () => true; | |
function useAsync<Result: any>( | |
promise: Input<Result>, | |
deps?: Array<any>, | |
shouldRun: ShouldRun = alwaysRun, | |
): UsePromiseState<Result> { | |
const skip = React.useRef(false); | |
const innerArg: any = | |
typeof promise === 'function' | |
? () => { | |
if (!shouldRun()) { | |
skip.current = true; | |
return null; | |
} | |
skip.current = false; | |
const func: InputFunc<Result> = (promise: any); | |
let result = func({ | |
skip: () => { | |
skip.current = true; | |
return new Skip(); | |
}, | |
}); | |
if (result && result instanceof Option) { | |
result = result.match({ | |
Some: (value) => value, | |
None: () => { | |
skip.current = true; | |
return new Skip(); | |
}, | |
}); | |
} | |
if (result instanceof Promise) { | |
return result; | |
} | |
return Promise.resolve(result); | |
} | |
: promise; | |
let didTransitionToRunning = false; | |
if (!shouldRun()) { | |
skip.current = true; | |
} else if (skip.current) { | |
didTransitionToRunning = true; | |
} | |
const [result, error, state] = usePromise<Result>((innerArg: any), deps); | |
const state2 = | |
skip.current || result instanceof Skip | |
? 'initial' | |
: didTransitionToRunning | |
? 'pending' | |
: state; | |
let lastResult: { current: Result | null } = React.useRef(null); | |
let lastError: { current: any } = React.useRef(null); | |
const service = React.useMemo(() => { | |
if (result != null) { | |
lastResult.current = result; | |
} | |
if (error != null) { | |
lastError.current = error; | |
} | |
return new UsePromiseState(lastResult.current, lastError.current, state2); | |
}, [result, error, state2]); | |
return service; | |
} | |
export default useAsync; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Based on https://github.com/bsonntag/react-use-promise/blob/3ab9f8731e69ce9be68f93469b1c5d828fa73521/src/index.js | |
// but with a bug fix for having an instant transition to pending/loading when the effect runs. | |
import { useEffect, useReducer } from 'react'; | |
function resolvePromise(promise) { | |
if (typeof promise === 'function') { | |
return promise(); | |
} | |
return promise; | |
} | |
const states = { | |
pending: 'pending', | |
rejected: 'rejected', | |
resolved: 'resolved', | |
}; | |
const STATE_PENDING = { | |
error: undefined, | |
result: undefined, | |
state: states.pending, | |
}; | |
function reducer(state, action) { | |
switch (action.type) { | |
case states.pending: | |
return STATE_PENDING; | |
case states.resolved: | |
return { | |
error: undefined, | |
result: action.payload, | |
state: states.resolved, | |
}; | |
case states.rejected: | |
return { | |
error: action.payload, | |
result: undefined, | |
state: states.rejected, | |
}; | |
/* istanbul ignore next */ | |
default: | |
return state; | |
} | |
} | |
function usePromise(_promise, inputs) { | |
let promise = _promise; | |
const forcePending = React.useRef(false); | |
const [{ error, result, state }, dispatch] = useReducer(reducer, { | |
error: undefined, | |
result: undefined, | |
state: states.pending, | |
}); | |
React.useMemo(() => { | |
forcePending.current = true; | |
}, inputs); | |
useEffect(() => { | |
promise = resolvePromise(promise); | |
if (!promise) { | |
return undefined; | |
} | |
let canceled = false; | |
dispatch({ type: states.pending }); | |
promise.then( | |
(res) => { | |
forcePending.current = false; | |
if (!canceled) { | |
dispatch({ | |
payload: res, | |
type: states.resolved, | |
}); | |
} | |
}, | |
(err) => { | |
forcePending.current = false; | |
if (!canceled) { | |
dispatch({ | |
payload: err, | |
type: states.rejected, | |
}); | |
} | |
}, | |
); | |
return () => { | |
canceled = true; | |
}; | |
}, inputs); | |
if (forcePending.current) { | |
return [STATE_PENDING.result, STATE_PENDING.error, STATE_PENDING.state]; | |
} | |
return [result, error, state]; | |
} | |
export default usePromise; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment