Question: How should libraries export custom hooks?
See original GitHub issueI’ve just hooked into React Hooks and done some experiments. I’m having some doubts about React’s advice on Building My Own Hooks.
Documentations tells me that,
A custom Hook is a JavaScript function whose name starts with “use” and that may call other Hooks.
So the idea I get is that, if I were to write an NPM package exporting a custom hook, I have to export a function and add React as a dependency to my package. Let’s take a couple of examples:
// Example 01 (from https://reactjs.org/docs/hooks-custom.html):
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
// Example 02 (detect user pressing escape key):
function handleEscapeKey (e) {
if (e.key === 'Escape') { /* do something with escape key */ }
}
useEffect(() => {
window.addEventListener('keydown', handleEscapeKey);
return () => {
window.removeEventListener('keydown', handleEscapeKey);
}
});
So if I were to create two NPM packages for these, I would first import React as a dependency,
import { useState, useEffect } from 'react';
And then implement the packages in this manner:
// Example 01:
export function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
// Example 02
export function useWindowKeydown(handler) {
useEffect(() => {
window.addEventListener('keydown', handler);
return () => {
window.removeEventListener('keydown', handler);
}
});
}
However, I feel that these packages are trying to do too much. For example,
- In the first example the custom hook is initializing the state, which may be better done by a component.
- Also, why do they both have to be aware about React. They are both trying to abstract away a pair of subscribe/unsubscribe functions.
Rather, we could export just the functionality we are trying to abstract away. And if we need any state or state-setters, we can pass them as arguments.
// Example 01: package `hook-friend-status`
export function hookFriendStatus(friendId, handleStatusChange) {
return () => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
}
}
// Example 02: package `hook-window-keydown`
export function hookWindowKeydown(handler) {
return () => {
window.addEventListener('keydown', handler);
return () => {
window.removeEventListener('keydown', handler);
}
}
}
And then we can use it inside our component in the following manner:
import React, { useState, useEffect } from 'react'
import hookFriendStatus from 'hook-friend-status';
import hookWindowKeydown from 'hook-window-keydown';
function MyAwesomeComponent ({ friendId }) {
const [isOnline, setIsOnline] = useState(
// component can decide how to initialize state.
false
);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(hookFriendStatus(friendId, handleStatusChange));
function handleEscapeKey (e) {
if (e.key === 'Escape') { /* do something with escape key */ }
}
useEffect(hookWindowKeydown(handleEscapeKey));
return (/** jsx */)
}
I think this way is much cleaner:
- The concerns are separated better.
- Packages/libraries don’t have to be aware of React (which also means these libraries can be used outside React as well).
- I would presume it will be easier to test the packages/libraries, because they are not calling react hooks (less mocking).
- Linter can stop searching after they scanned the component, so could the humans when doing the code review.
It might be a premature judgement that I have reached, as we are yet to see what people are exactly doing with hooks. Let me know what you guys think. Thanks.
Issue Analytics
- State:
- Created 5 years ago
- Comments:7 (3 by maintainers)
The way I see it, it’s not separation of concerns. You are specifically foisting all the concerns that belong to how your hook operates into the users of your hook instead, making it significantly easier to accidentally misuse them.
Personally, I try to stick as close as possible to the patterns exposed by
useEffect
,useContext
anduseReducer
(oruseState
which is justuseReducer
with a default reducer):useEffect
-like: has side-effects only. Returnsvoid
, which indicates a side-effect.useContext
-like: always returns a value that is current at the time of render.useReducer
-like: always returns a pair; the first value is a value that might be different on each render, the second value should be immutable (same reference on every invocation of the reducer). More return values can be used, making it an n-tuple instead of pair (2-tuple), if needed, but I don’t think there are patterns for those yet.If your hook accepts a function callback, you should also accept an inputs array and feed them both to
useCallback
for internal use. Don’t assume your user has already passed the function touseCallback
.All custom hooks should handle internally all aspects related to initializing and disposing of their resources and side-effects. The only thing the user of the custom hook should be concerned about is its input arguments and its result, not about which hooks are used internally to implement the result.
Here is an example of how Preact does it: https://preactjs.com/guide/switching-to-preact
In practice we’re already seeing multiple incompatible implementations, e.g. https://github.com/yyx990803/vue-hooks, https://github.com/matthewp/haunted, https://github.com/getify/TNG-Hooks. They’re not quite the same. This will take a lot of time to shake out and experiment with. If eventually there’s some kind of consensus there’s nothing preventing us on pulling out the dispatcher into a separate lib (e.g.
require('hooks')
, name TBD) and React just re-exporting those. But we’re far from that convergence now.