Skip to content

Instantly share code, notes, and snippets.

@brigand
Last active May 28, 2019 04:28
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 brigand/8d6efa0601661c77f22599e6fea666b8 to your computer and use it in GitHub Desktop.
Save brigand/8d6efa0601661c77f22599e6fea666b8 to your computer and use it in GitHub Desktop.
A react hook for an enum-like interface to async data

Simple example where .match is used to render in one of three states.

function User({ id }) {
  const state = usePromise(() => get('/api/user', { id }), [id]);
  
  return (
    <div className={css.root}>
      {state.match({
        success: (data) => <UserDisplay id={id} data={data} />,
        failure: (error) => <ErrorDisplay value={error} />,
        default: () => <Loading />,
      })}
    </div>
  );
}

Same component, but with some differences.

  • The loading can be displayed in addition to the data
  • The error is displayed only when in the failure state (.failure() is state-dependent)
  • UserDisplay (the value case) is rendered any time we are both 1. not in an error state, and 2. there has at some point been a value
  • The value case is not state-dependent, so it also returns a value if we went loading -> success -> loading.

Note: in this example the .match, .map, and .unwrap_or are methods of safe-types's Option - not specific to this hook.

function User({ id }) {
  const state = usePromise(() => get('/api/user', { id }, [id]));
  
  return (
    <div className={css.root}>
      {state.loading() && <Loading />}
      {state.failure().match({
        Some: error => <ErrorDisplay value={error} />,
        None: () => state.value()
          .map(data => <UserDisplay id={id} data={data} />)
          .unwrap_or(null),
      })}
    </div>
  );
}
// Implementation of the hook. Happens to be in typescript, but doesn't use typescript enums.
import * as React from 'react';
import usePromiseLib from 'react-use-promise';
import { Option } from 'safe-types';
type State = 'pending' | 'resolved' | 'rejected';
export class UsePromiseState<Result> {
_result: Result | null;
_error: unknown;
_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<unknown> {
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<unknown> {
return this.error().filter((x: unknown) => this._state === 'rejected');
}
// Get if we're current in a loading state
loading(): boolean {
return this._state === 'pending';
}
// 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: Partial<{
value: (value: Result) => Out;
error: (error: unknown) => Out;
success: (value: Result) => Out;
failure: (value: unknown) => Out;
loading: () => 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 !== '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.`,
);
}
}
function usePromise<Result = any>(
promise: Promise<Result> | (() => Promise<Result>),
deps?: Array<any>,
): UsePromiseState<Result> {
const [result, error, state] = usePromiseLib<Result>(promise, deps);
let lastResult = React.useRef<Result | null>(null);
let lastError = React.useRef<any>(null);
const service = React.useMemo(() => {
if (result != null) {
lastResult.current = result;
}
if (error != null) {
lastError.current = error;
}
return new UsePromiseState(lastResult.current, lastError.current, state);
}, [result, error, state]);
return service;
}
export default usePromise;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment