Skip to content

Instantly share code, notes, and snippets.

@ajitid
Last active September 15, 2021 21:55
Show Gist options
  • Save ajitid/1daf9073b93ccc5079a5e9c3f79ed74f to your computer and use it in GitHub Desktop.
Save ajitid/1daf9073b93ccc5079a5e9c3f79ed74f to your computer and use it in GitHub Desktop.
useAsync + ky
import React, { useCallback, useState } from 'react'
import { useForm } from 'react-hook-form'
import useSignal from 'utils/hooks/useSignal'
import useAsync, { AsyncState } from 'utils/hooks/useAsync'
const UpdateUserForm = () => {
const signal = useSignal()
const { getValues, /* ... */ reset: resetForm } = useForm<FormData>({
validationSchema,
defaultValues: {
visible: false
},
})
// fetch data at start to put into form fields:
const initialFetchPromise = useCallback(async () => {
const result = await UserApi.get(userId, { signal })
return result
}, [signal, userId])
const { current: initialFetchCurrent, err: initialFetchErr, errJson: initialFetchErrJson } = useAsync(
initialFetchPromise,
{
// immediate with array means call `initialFetchPromise` by spreading array args at start
immediate: [],
onCurrentChange({ current, data, err }) {
switch (current) {
case AsyncState.Success:
const user = data!
setUser(user)
resetForm({
firstname: user.firstname,
lastname: user.lastname,
email: user.email,
visible: user.visible
// ...
})
break
}
},
}
)
// will use it to determine what to render
const isInitialFetchPending = initialFetchCurrent === AsyncState.Pending
// on form submission:
const formPromise = useCallback(async () => {
const values = getValues()
const fetchedUser = await UserApi.get(userId, { signal })
const result = await UserApi.update(
{
...fetchedUser,
id: fetchedUser.id,
firstname: values.firstname,
lastname: values.lastname,
email: values.email,
visible: values.visible,
// ...
},
{ signal }
)
return result
}, [getValues, signal, userId])
const { run: sendRequest, current, err, errJson } = useAsync(formPromise, {
onCurrentChange({ current, data, err }) {
switch (current) {
case AsyncState.Success:
const user = data!
setUser(user)
resetForm({
firstname: user.firstname,
lastname: user.lastname,
email: user.email,
visible: user.visible
// ...
})
break
}
},
})
const isPending = current === AsyncState.Pending
const isSuccess = current === AsyncState.Success
const onSubmit = handleSubmit(async () => {
await sendRequest()
})
const Comp = () => {
// ...
const debouncedCallback = useRef(
debounce((text: string, otherData: Data | null) => {
// sendRequest = run
sendRequest(
{
text,
pageNo: 0,
otherData,
},
true
)
}, 400)
)
const textFilter = useInput(
'',
e => {
debouncedCallback.current(e.target.value, otherData)
},
() => {
debouncedCallback.current('', otherData)
}
)
// ...
}
// useInput.ts
import React, { useState } from 'react'
import { noop } from 'utils/helpers'
const useInput = (
initialValue = '',
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void = noop,
onReset: () => void = noop
) => {
const [value, setValue] = useState(initialValue)
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value)
onChange(e)
}
const resetValue = () => {
setValue('')
onReset()
}
return { value, onChange: handleChange, resetValue }
}
export default useInput
import { useRef, useEffect, useCallback, useState } from 'react'
import { HTTPError } from 'ky'
import { noop } from 'utils/helpers'
export enum AsyncState {
Initial = 'initial',
Pending = 'pending',
Success = 'fulfilled',
Error = 'rejected',
}
interface PromiseFnShape {
(...args: any): Promise<any>
}
type Unpromisify<T> = T extends Promise<infer U> ? U : T
interface SetStatesContextShape<R> {
current: AsyncState
data: R | null
err: Error | null
}
interface SetStatesShape<F extends PromiseFnShape> {
(context: SetStatesContextShape<Unpromisify<ReturnType<F>>>, params: Parameters<F>): void
}
interface UseAsyncOptions<F extends PromiseFnShape> {
signal?: AbortSignal
immediate?: Parameters<F> | false
onCurrentChange?: SetStatesShape<F>
}
function useAsync<F extends PromiseFnShape>(
promiseFn: F,
{ immediate = false, onCurrentChange = noop, signal }: UseAsyncOptions<F> = {}
) {
const hasRanOnce = useRef(false)
function generateKey() {
return Date.now() + Math.round(Math.random() * 10000)
}
const [current, _setCurrent] = useState<AsyncState>(AsyncState.Initial)
const [key, setKey] = useState<number>(() => generateKey())
function setCurrent(current: AsyncState, key: number) {
_setCurrent(current)
setKey(key)
}
const data = useRef<Unpromisify<ReturnType<F>> | null>(null)
const err = useRef<Error | null>(null)
const [errJson, setErrJson] = useState<any>(null)
useEffect(() => {
switch (current) {
case AsyncState.Error:
if (err.current instanceof HTTPError) {
const res = err.current.response.clone()
res
.json()
.then(json => {
setErrJson(json)
})
.catch(() => {
setErrJson(null)
})
}
break
default:
setErrJson(null)
break
}
}, [current])
const setStates: SetStatesShape<F> = ({ current: newCurrent, data: newData, err: newErr }, params) => {
onCurrentChange({ current: newCurrent, data: newData, err: newErr }, params)
switch (newCurrent) {
case AsyncState.Success:
data.current = newData
break
case AsyncState.Error:
err.current = newErr
break
}
if (signal?.aborted) return // if success but later the signal is aborted
setCurrent(newCurrent, generateKey())
}
const run = useCallback(
async function(...params: Parameters<F>) {
setStates({ current: AsyncState.Pending, data: null, err: null }, params)
promiseFn(...params)
.then(res => {
setStates({ current: AsyncState.Success, data: res, err: null }, params)
})
.catch((e: Error) => {
if (e.name === 'AbortError') return
setStates({ current: AsyncState.Error, data: null, err: e }, params)
})
},
[promiseFn] /* eslint-disable-line react-hooks/exhaustive-deps */
)
useEffect(() => {
if (immediate !== false && !hasRanOnce.current) {
hasRanOnce.current = true
run(...immediate)
}
}, [immediate, run])
return { current, run, key, data: data.current, err: err.current, errJson }
}
export default useAsync
import { useRef, useEffect } from 'react'
const useSignal = () => {
const controller = useRef(new AbortController())
useEffect(() => {
return () => {
controller.current.abort() /* eslint-disable-line */
}
}, [])
return controller.current.signal
}
export default useSignal
@ajitid
Copy link
Author

ajitid commented Sep 15, 2021

This was one of my very early approach of creating a useAsync hook. I would change many things here now, for example using .then(resolveFn, rejectFn) instead of .then(resolveFn).catch(rejectFn) in the hook or just using a simplified hook for simpler cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment