One of the snippet's I picked up from Kent C. Dodds' EpicReact course was this super cool useAsync function, and it's a masterclass in elegance. There are several patterns I want to call out that he uses here that every React dev should know.
Without further ado, here's the code for you to skim. We'll break it down in a second.
function asyncReducer(state, action) {
switch (action.type) {
case 'pending': {
return {status: 'pending', data: null, error: null}
}
case 'resolved': {
return {status: 'resolved', data: action.data, error: null}
}
case 'rejected': {
return {status: 'rejected', data: null, error: action.error}
}
default: {
throw new Error('Unhandled action type')
}
}
}
function useAsync(initialState) {
const [state, unsafeDispatch] = React.useReducer(asyncReducer, {
status: 'idle',
data: null,
error: null,
...initialState,
})
const dispatch = useSafeDispatch(unsafeDispatch)
const {data, error, status} = state
const run = React.useCallback(
promise => {
dispatch({type: 'pending'})
promise.then(
data => {
dispatch({type: 'resolved', data})
},
error => {
dispatch({type: 'rejected', error})
},
)
},
[dispatch],
)
const setData = React.useCallback(
data => dispatch({type: 'resolved', data}),
[dispatch],
)
const setError = React.useCallback(
error => dispatch({type: 'rejected', error}),
[dispatch],
)
return {
setData,
setError,
error,
status,
data,
run,
}
}
This one is great. All too often I've seen people return their state setting function from their hook without wondering whether it could be abstracted further.
function useAsync(initialState) {
const [state, dispatch] = React.useReducer(asyncReducer)
//...do some stuff
return {
state,
dispatch
}
}
Look familiar? Well it turns out we can do better. Much better.
Let's take a look at the code someone using this hook would have to write:
const [state, dispatch] = useAsync()
React.useEffect(() => {
dispatch({type: 'pending'})
fetch("https://api.example.com/items").then(
data => {
dispatch({type: 'resolved', data})
},
error => {
dispatch({type: 'rejected', error})
},
)
},[])
Something like this. They have to run the fetch themselves, and resolve the promise with a dispatch. Is this the end of the world?
Of course not.
But why are we bothering them with our action types? We know everything that has to happen when that fetch is called.. Why not do it for them?
Enter Kent's run function:
const run = React.useCallback(
promise => {
dispatch({type: 'pending'})
promise.then(
data => {
dispatch({type: 'resolved', data})
},
error => {
dispatch({type: 'rejected', error})
},
)
},
[dispatch],
)
We accept a promise, and handle things accordingly when it resolves. All of the chaining and worrying about action types has moved from our fellow dev's component into our hook. Neat!
This is how their implementation looks now:
const {state, run} = useAsync()
React.useEffect(() => {
run(fetch("https://api.example.com/items"))
}, [])
Beautiful! But what if they need to set state manually? This brings me to our third pattern:
The run function makes it so we actually NEED to use this pattern, but it's a great one to keep in mind even when you don't need it because it is more widely applicable than the run function we just went over.
We aren't providing our fellow devs a dispatch anymore, so they have no way to force state. We need to provide one, so we'll just make some setters.
const setData = React.useCallback(
data => dispatch({type: 'resolved', data}),
[dispatch],
)
const setError = React.useCallback(
error => dispatch({type: 'rejected', error}),
[dispatch],
)
Hopefully you learned something here to help you write hooks a little bit more elegantly :)
I know I glossed over creating the reducer and some pieces here. Really I just wanted to call out these few patterns but if you have questions, feel free to email me at reid@43webstudio.com.