Custom Hook for Async Effects
In React, handling asynchronous side effects within useEffect can become tricky. A custom hook can simplify the process and make your code more reusable. Here’s how you can build a custom hook that handles async effects, including managing loading states and cleaning up side effects when the component unmounts.
Basic useAsync Hook
The useAsync hook helps manage the lifecycle of an async operation, providing loading (pending), success (resolved), and error (rejected) states.
function useAsync(asyncCallback) {
let [state, dispatch] = React.useReducer(asyncReducer)
React.useEffect(() => {
let promise = asyncCallback()
if (!promise) return
dispatch({ type: 'pending' })
promise
.then((data) => dispatch({ type: 'resolved', data }))
.catch((error) => dispatch({ type: 'rejected', error }))
}, [asyncCallback])
return state
}
- Key Components:
useReducerto manage different states (idle, pending, resolved, rejected).useEffectto trigger the async job when the component mounts or updates.
Usage Example
function Component({ input }) {
// Wrap the async job in a useCallback to avoid unnecessary re-renders
let asyncCallback = React.useCallback(() => {
if (!input) return
// Execute async operation (fetch is used as an example)
return fetch(input)
}, [input])
let { status, data, error } = useAsync(asyncCallback)
switch (status) {
case 'idle':
return 'Waiting for the async to trigger'
case 'pending':
return 'Pending UI'
case 'rejected':
throw error
case 'resolved':
return 'Data UI'
default:
throw new Error('This should be impossible')
}
}
- How It Works:
- The
useCallbackensures theasyncCallbackonly changes wheninputchanges, preventing unnecessary re-renders. - Based on the
status, different UI components (loading, error, success) are rendered.
- The
Cleaning Up Side Effects: useSafeDispatch
When dealing with async operations, it’s important to clean up side effects if the component unmounts before the operation completes. This can be handled by a useSafeDispatch hook, which ensures that dispatch is only called if the component is still mounted.
function useSafeDispatch(dispatch) {
let mountedRef = React.useRef(false)
React.useEffect(() => {
mountedRef.current = true
return () => {
mountedRef.current = false
}
}, [])
return React.useCallback(
(...args) => {
if (mountedRef.current) {
dispatch(...args)
}
},
[dispatch]
)
}
- Explanation:
- The
mountedReftracks whether the component is mounted or unmounted. useSafeDispatchprevents state updates on unmounted components, avoiding memory leaks and React warnings.
- The
Enhanced useAsync with Safe Dispatch
Now, let’s modify the useAsync hook to incorporate useSafeDispatch, making it safer for unmounting scenarios.
function useAsync(asyncCallback) {
let [state, unsafeDispatch] = React.useReducer(asyncReducer)
let dispatch = useSafeDispatch(unsafeDispatch)
React.useEffect(() => {
let promise = asyncCallback()
if (!promise) return
dispatch({ type: 'pending' })
promise
.then((data) => dispatch({ type: 'resolved', data }))
.catch((error) => dispatch({ type: 'rejected', error }))
}, [asyncCallback])
return state
}
- How It Works:
useSafeDispatchensures thatdispatchis only executed if the component is mounted, preventing any updates to unmounted components.
Practical Use Cases
- Fetching Data: You can use this hook to fetch data from an API and handle loading, success, and error states effectively.
- Asynchronous Validations: In forms, async validations (e.g., checking if an email already exists) can be handled using this pattern.
- Cleanup of Async Jobs: Safely managing async operations and ensuring no memory leaks in large-scale applications.
Conclusion
The useAsync hook and useSafeDispatch are essential tools for handling async side effects in React, especially when the component lifecycle is involved. By safely managing dispatches and cleanups, you can avoid common pitfalls like memory leaks and unhandled promises.
Happy coding!
A simple JavaScript utility to check if a string is a valid CSS color: Color validator