23
loading...
This website collects cookies to deliver better user experience
useAsync()
hook that we developed in the previous part of the series but you don't need to read them in order to move ahead. This can be treated as a standalone blog on its own but I am categorising it as part 3 of our useAsync()
hook series.useAsync
hook looks like this:function useSafeDispatch(dispatch) {
const mounted = React.useRef(false)
React.useLayoutEffect(() => {
mounted.current = true
return () => (mounted.current = false)
}, [])
return React.useCallback(
(...args) => (mounted.current ? dispatch(...args) : void 0),
[dispatch],
)
}
const defaultInitialState = {status: 'idle', data: null, error: null}
function useAsync(initialState) {
const initialStateRef = React.useRef({
...defaultInitialState,
...initialState,
})
const [{status, data, error}, setState] = React.useReducer(
(s, a) => ({...s, ...a}),
initialStateRef.current,
)
const safeSetState = useSafeDispatch(setState)
const setData = React.useCallback(
data => safeSetState({data, status: 'resolved'}),
[safeSetState],
)
const setError = React.useCallback(
error => safeSetState({error, status: 'rejected'}),
[safeSetState],
)
const reset = React.useCallback(
() => safeSetState(initialStateRef.current),
[safeSetState],
)
const run = React.useCallback(
promise => {
if (!promise || !promise.then) {
throw new Error(
`The argument passed to useAsync().run must be a promise. Maybe a function that's passed isn't returning anything?`,
)
}
safeSetState({status: 'pending'})
return promise.then(
data => {
setData(data)
return data
},
error => {
setError(error)
return Promise.reject(error)
},
)
},
[safeSetState, setData, setError],
)
return {
isIdle: status === 'idle',
isLoading: status === 'pending',
isError: status === 'rejected',
isSuccess: status === 'resolved',
setData,
setError,
error,
status,
data,
run,
reset,
}
}
export {useAsync}
Error: Invalid hook call
. Create a test component that uses the hook in the typical way the hook would
be used by consumers and test that component.
react-testing-library
.Promise
behavior.function deferred() {
let resolve, reject
const promise = new Promise((res, rej) => {
resolve = res
reject = rej
})
return {promise, resolve, reject}
}
const {promise, resolve,reject} = deferred()
//resolve
const fakeResolvedValue = Symbol('some resolved value')
run(promise)
resolve(resolvedValue)
await promise
//reject
const rejectedValue = Symbol('rejected value')
run(promise)
reject(rejectedValue)
await promise.catch(() => {
/* ignore error */
})
import {renderHook} from '@testing-library/react-hooks'
import {useAsync} from '../hooks'
test('calling run with a promise which resolves', async () => {
const {promise, resolve} = deferred()
//this is how we can render the hook using the library
const {result} = renderHook(() => useAsync())
//try console logging result.current and see what exactly is the result object
console.log(result)
}
{
isIdle: true,
isLoading: false,
isError: false,
isSuccess: false,
setData: [Function (anonymous)],
setError: [Function (anonymous)],
error: null,
status: 'idle',
data: null,
run: [Function (anonymous)],
reset: [Function (anonymous)]
}
Function(anonymous)
is not of our concern, basically, it says that it is some function and we don't need to know much more than that. So, we will assert them using expect.any(Function)
and our job is done.const defaultState = {
status: 'idle',
data: null,
error: null,
isIdle: true,
isLoading: false,
isError: false,
isSuccess: false,
run: expect.any(Function),
reset: expect.any(Function),
setData: expect.any(Function),
setError: expect.any(Function),
}
const pendingState = {
...defaultState,
status: 'pending',
isIdle: false,
isLoading: true,
}
const resolvedState = {
...defaultState,
status: 'resolved',
isIdle: false,
isSuccess: true,
}
const rejectedState = {
...defaultState,
status: 'rejected',
isIdle: false,
isError: true,
}
test('calling run with a promise which resolves', async () => {
const {promise, resolve} = deferred()
const {result} = renderHook(() => useAsync())
expect(result.current).toEqual(defaultState)
/* we will pass our promise to run method and check if we are getting
pending state or not */
let p
act(() => {
p = result.current.run(promise)
})
expect(result.current).toEqual(pendingState)
/* We are resolving our promise and asserting if the value is
equal to resolvedValue */
const resolvedValue = Symbol('resolved value')
await act(async () => {
resolve(resolvedValue)
await p
})
expect(result.current).toEqual({
...resolvedState,
data: resolvedValue,
})
// asserting if reset method is working or not
act(() => {
result.current.reset()
})
expect(result.current).toEqual(defaultState)
})
act
here?act() makes sure that anything that might take time - rendering, user events, data fetching - within it is completed before test assertions are run.
test('calling run with a promise which rejects', async () => {
const {promise, reject} = deferred()
const {result} = renderHook(() => useAsync())
expect(result.current).toEqual(defaultState)
let p
act(() => {
p = result.current.run(promise)
})
expect(result.current).toEqual(pendingState)
/* same as our first test till now but now we will reject the promise
assert for rejectedState with our created rejected value */
const rejectedValue = Symbol('rejected value')
await act(async () => {
reject(rejectedValue)
await p.catch(() => {
/* ignore error */
})
})
expect(result.current).toEqual({...rejectedState, error: rejectedValue})
})