Created
March 18, 2020 21:50
-
-
Save steida/79c92411b8a7d88454372f1a388fccb6 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
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