Two simple tips on using React Hooks
February 15, 2020
I have been using React Hooks for a few months now and I am absolutely loving it! Today I would like to share 2 simple tips that I have learned along the way. Hopefully you will find them useful!
useEffect for API requests
One common use case for useEffect
hook is to fetch some data from REST API and display them:
useEffect(() => {
const fetchUserData = async () => {
const response = await API.getUserData(userId);
setUserData(response.userData);
};
fetchUserData();
}, [userId);
This looks fine at first glance but there are actually 2 potential bugs here:
- If user navigate away before the API returns, this will cause a set state to an unmounted component which may lead to memory leak.
- If
userId
changes before the previous request returns, this will cause the component to send another request which may lead to race condition (The first request returns after the second one and override the state).
To prevent those bugs we can simply add a local variable ignore
:
useEffect(() => {
let ignore = false;
const fetchUserData = async () => {
const response = await API.getUserData(userId);
if (!ignore) {
setUserData(response.userData);
}
};
fetchUserData();
return () => {
ignore = true;
};
}, [userId);
ignore
will be set to true when ever the component is unmounted or the dependency array has changed (userId
changed in this case). Therefore setUserData
will not be called!
This pattern is documented in React’s official documentation.
useDeepMemo
Sometimes when we write custom hooks we want that hook to be able to take a dynamic object or array as input. For example consider this useAPI
custom hook:
// usage
const response = useAPI({
url: ‘/api/get-user’,
body: { userId },
});
// implementation
function useAPI({ url, body }) {
const [response, setResponse] = useState(null);
useEffect(() => {
// do the request here
}, [url, body]);
return response;
}
This is a nice handy custom hook, but there is just one problem. Whenever there is a re-render, body
is passed in as a new object and useEffect
will get triggered again. This will cause an infinite loop!
Sure, we can wrap the body object in an useMemo
hook, but it becomes a pain in the ass when you have to do that every time you use useAPI
!
To solve this, we can write another custom hook useDeepMemo
:
// you can also use other deep compare functions
// e.g. lodash’s _.isEqual
import { equal } from ‘@wry/equality’;
function useDeepMemo(memoFn, key) {
const ref = useRef();
if (!ref.current || !equal(key, ref.current.key)) {
ref.current = { key, value: memoFn() };
}
return ref.current.value;
}
I first saw this technique from Apollo’s source code. This is a replacement for useMemo
but it uses deep equality to compare memo keys and it guarantees that the memo function will only be called if the keys are unequal.
With that, we can then rewrite our useAPI
hook:
function useAPI({ url, body }) {
const [response, setResponse] = useState(null);
const cachedBody = useDeepMemo(() => body, [body]);
useEffect(() => {
// do the request here
}, [url, cachedBody]);
return response;
}
Now we can use useAPI
without worrying about accidentally causing an infinite loop!