Created
March 18, 2020 21:50
-
-
Save steida/79c92411b8a7d88454372f1a388fccb6 to your computer and use it in GitHub Desktop.
Revisions
-
steida created this gist
Mar 18, 2020 .There are no files selected for viewing
This file contains hidden or 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 charactersOriginal 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; };