Skip to content

Instantly share code, notes, and snippets.

@steida
Created March 18, 2020 21:50
Show Gist options
  • Save steida/79c92411b8a7d88454372f1a388fccb6 to your computer and use it in GitHub Desktop.
Save steida/79c92411b8a7d88454372f1a388fccb6 to your computer and use it in GitHub Desktop.
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;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment