When should I use a custom Hook? #52
Replies: 3 comments 6 replies
-
I don't think the term "primitive hooks" is in the docs anywhere, but I've certainly been using it and I think a number of people in the community have as well. I think the terms "built-in hooks" and "primitive hooks" would be equivalent - hooks that are A) part of the React package itself, and B) not something that can be reproduced in userland because they have to be provided by the runtime. These hooks are the "primitives" that the community can use to build further abstractions. |
Beta Was this translation helpful? Give feedback.
-
I think there are a few different ways to interpret "primitive Hook" but I agree that almost everyone using this term means "built-in Hook", which is a clearer name, and to @eps1lon's point, harder to misread as pejorative. (Aside: In the strictest sense, I interpret "primitive Hook" as tech jargon that means "Hook that isn't made out of other Hooks". Using that definition, useState would technically not be a primitive Hook because under the hood it's currently built on top of useReducer! But in practice I think when people colloquially use this phrase they mean to include all the Hooks that React itself provides, which is why "built-in" is probably more appropriate.) Asking "when it it appropriate to use a built-in Hook over a custom one I've made?" is a lot like the question "when is it appropriate to use a function built into the browser over a custom util I've made?". I find advice like "never use useState directly" pretty strange, just as I would find it strange if someone said "never use Object.keys() directly". The vast majority of React components that I write use built-in Hooks, and now and then I'll use a custom Hook either from my own codebase or from a third-party npm package. My advice to people asking "when is it appropriate to split my Hooks logic into a separate function (i.e., into a custom Hook)?" is the same as it is for someone asking "when is it appropriate to split my logic into a separate function?" in a non-React context. Most of the time, I'll start without defining lots of nested functions. I try to write my code in the clearest way possible (not the "cleanest" way), and then only add extra structure if it's needed. Sometimes that's because I notice the same logic that's needed in multiple components and want to be able to reuse it in multiple places. But that works best when it's easy to understand and explain what that piece of the code does. For example, I might split out some code like // (nb: for full compatibility with concurrent features, this could use useMutableSource instead)
let [width, setWidth] = useState(window.innerWidth);
useLayoutEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []); into a custom Hook so that I can call just let width = useWindowWidth(); in multiple components. I might even do this refactor if the logic appears only in a single component, and leave the definition of the custom Hook within the same file that the component is in. But I think this works well because it's easy to explain what this chunk of behavior does in an isolated way – it returns the current window width and rerenders the component when that changes – whereas one pitfall that you can end up with is if you split some logic to a custom Hook, but someone reading the code needs to go read the source code of that custom Hook to understand what's going on, then maybe it would've been clearer to leave the code inline in the main component in the first place. The last thing I'll mention is that you need to be careful about creating a steeper-than-necessary learning curve for people working in your codebase. In order to be productive in a React web codebase, someone generally needs to be familiar with the built-in browser APIs as well as built-in React APIs. If you add another layer on top of that that people also need to learn, then people who know browser APIs and React APIs but not your custom layer won't be able to be as productive on day 1. That isn't to say either that you should always avoid creating functions to encapsulate pieces of logic – in fact, the fact that Hooks allows you to define your own version of built-in React features is one of the things I really liked about them when we originally discussed adding them to React in early 2018 – but just to remember that each layer you add comes with a cost and it's important to make sure you're removing more complexity than you're adding. (It's tempting to think that you'll be able to create a layer such that no one needs to worry about the details underneath – but in my experience that's a lot harder than it sounds and the lower-level details leak through anyway, so it's usually more sustainable to try to work with the lower layer (in this case, React itself and the built-in Hooks) rather than hiding them in a corner.) |
Beta Was this translation helpful? Give feedback.
-
And even that is not a hard recommendation. Just something to consider. Custom Hooks for context like But overall, using the built-ins is completely fine. |
Beta Was this translation helpful? Give feedback.
-
I don't love the word "never".
I've been hearing a "never use a 'primitive' Hook" mantra crop up in conversations and recommendation across Twitter. I called it out of Twitter and @rickhanlonii said I should bring it up here 😁
I have a few questions:
1. What is the sectioned term for built-in Hooks?
Is "primitive Hook" a supported terminology alias for built-in Hooks?
Should we gently correct to "built-in Hook"?
The Hooks API Reference document uses these terms:
I know this is a very nuanced thing but community language can help reduce misconceptions. Closely aligning to documented and internal terminology makes things easy to find and allows y'all space to manipulate language from version to version.
2. Is there an official recommendation on when to use "built-in Hooks" directly?
(This question has a lot of nuance and is challenging to ask clearly. I'm going to try my best.)
In his response on render-commit phases, @acdlite said:
This makes sense to me for
useEffect
— full stop.When I read that I read it at advice, I read it exclusive to
useEffect
and complex custom Hooks (composing different built-in Hooks) whereuseEffect
is used. However, it does leave a window open for a — growing — "never use a 'primitive' Hook" mantra.There's a less dogmatic truth here that "abstracting Hooks — whether for re-use or encapsulation — is generally a good practice". But there's lot of nuance.
useEffect
is well-suited for custom hook wrappers, e.g.,useHotKey
,useBoxSize
,useScrollPosition
, etc.useReducer
is well-suited to for direct use with extracted reducers, e.g.,useReducer(todoListReducer, …)
,useReducer(videoPlaybackReducer, …)
useContext
is well-suited for direct use, e.g.,useContext(TodoList)
,useContext(SystemPermissions)
, etc.Examining my own code, I'll often use components — not custom Hooks — to abstract complex Hook compositions using a "controller" pattern shared by @bvaughn here:
At the end of the day, I think my question is this:
Ya'll have a lot of experience in updating facebook.com. This experience has deeply informed the design of React APIs. But are there also recommendations for folks looking for "a better way to live"?
This is a terrible analogy but here goes:
In lawn care, they say the best defense against weeds is a healthy, lush lawn. "Never use a 'primitive' Hook" feels like weed. And it's taking root because there is a lack of nuanced guidance for custom Hook design.
If this is something we need to take on in education, can you share with us what the most successful and least successful patterns have been?
Thanks in advance for the additional grace extended to these ill-formed inquiries 🙏
Beta Was this translation helpful? Give feedback.
All reactions