Skip to content

Instantly share code, notes, and snippets.

@steida
Created March 18, 2020 21:50
Show Gist options
  • Select an option

  • Save steida/79c92411b8a7d88454372f1a388fccb6 to your computer and use it in GitHub Desktop.

Select an option

Save steida/79c92411b8a7d88454372f1a388fccb6 to your computer and use it in GitHub Desktop.

Revisions

  1. steida created this gist Mar 18, 2020.
    150 changes: 150 additions & 0 deletions useMutation.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,150 @@
    import * as E from 'fp-ts/lib/Either';
    import { absurd, constVoid } from 'fp-ts/lib/function';
    import { pipe } from 'fp-ts/lib/pipeable';
    import * as TE from 'fp-ts/lib/TaskEither';
    import * as t from 'io-ts';
    import { useCallback } from 'react';
    import { Api, ApiError, FetchError } from '../types';
    import { Form, useForm } from './useForm';

    // Basically, every HTTP request is a mutation.
    // The difference is the usage on the client.
    // That's why we have useMutation and useQuery.

    export const useMutation = <
    Name extends keyof Api,
    Endpoint extends typeof Api['props'][Name]
    >(
    name: Name,
    initialState: t.OutputOf<Endpoint['props']['input']>,
    {
    handleError,
    handleSuccess,
    }: {
    handleError?: (
    error: t.TypeOf<Endpoint['props']['error']>,
    form: Form<Endpoint['props']['input']['props']>,
    ) => void;
    handleSuccess?: (
    payload: t.TypeOf<Endpoint['props']['payload']>,
    form: Form<Endpoint['props']['input']['props']>,
    ) => void;
    },
    ): Form<Endpoint['props']['input']['props']> => {
    const endpoint = Api.props[name];

    const submit = useCallback(
    (form: Form<Endpoint['props']['input']['props']>) => (
    data: t.TypeOf<Endpoint['props']['input']>,
    ) => {
    if (form.isDisabled) return;
    form.disable();

    // Generics within functions suck. We have to retype decode.
    // https://github.com/gcanti/fp-ts/issues/904#issuecomment-558528906
    const decode: (
    response: unknown,
    ) => E.Either<
    t.Errors | FetchError,
    E.Either<
    t.TypeOf<Endpoint['props']['error']>,
    t.TypeOf<Endpoint['props']['payload']>
    >
    > = endpoint.props.output.decode;

    const handleClientServerMismatch = () => {
    if (confirm('App is outdated. Confirm to auto update.')) {
    // Never do this automatically.
    location.reload(true);
    }
    };

    const handleFetchError = (error: t.Errors | FetchError) => {
    if (FetchError.is(error)) {
    alert(`Please check network connection. Error: ${error.message}`);
    return;
    }
    handleClientServerMismatch();
    };

    const handleApiError = (error: ApiError) => {
    switch (error.status) {
    case 'badRequest':
    handleClientServerMismatch();
    break;
    case 'forbidden':
    case 'internalServerError':
    case 'notFound':
    case 'unauthorized':
    alert(error.status + ' ' + error.message);
    break;
    default:
    absurd(error.status);
    }
    };

    const handleFetchResponse = (
    output: E.Either<
    t.TypeOf<Endpoint['props']['error']>,
    t.TypeOf<Endpoint['props']['payload']>
    >,
    ) => {
    pipe(
    output,
    E.fold(
    error => {
    if (handleError) handleError(error, form);
    if (ApiError.is(error)) {
    handleApiError(error);
    } else if (endpoint.props.formError.is(error)) {
    // as any, because I don't know
    form.setAsyncErrors(error.errors as any);
    }
    },
    payload => {
    if (handleSuccess) handleSuccess(payload, form);
    },
    ),
    );
    };

    TE.tryCatch<FetchError, unknown>(
    () =>
    fetch(`/api/${name}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
    }).then(response => response.json()),
    error => ({ type: 'fetchError', message: String(error) }),
    )().then(response => {
    form.enable();
    pipe(
    response,
    E.chain(decode),
    E.fold(handleFetchError, handleFetchResponse),
    );
    });
    },
    [
    handleError,
    handleSuccess,
    endpoint.props.formError,
    endpoint.props.output.decode,
    name,
    ],
    );

    const handleSubmit = useCallback(
    (form: Form<typeof endpoint.props.input.props>) => {
    pipe(form.validated, E.fold(constVoid, submit(form)));
    },
    [endpoint, submit],
    );

    // as any, because I don't know
    const form = useForm(endpoint.props.input as any, initialState, {
    handleSubmit,
    });

    return form;
    };