I'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,
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:
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.
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 and useReducer (or useState which is just useReducer with a default reducer):
useEffect-like: has side-effects only. Returns void, 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 to useCallback.
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.
I actually came to the Issues to file something else, but this caught my eye since I was just thinking about it earlier today. In fact I'm just going to copy-paste what I wrote elsewhere
I like the idea of "choose your framework" but I think that should be abstracted at a higher level...
In other words, it should be done by "hook compositions" authors - and there should be a standard interface in Typescript or whatever. There could be _no dependencies_ burnt in by those hook composition libraries- rather, they accept the dependency as a parameter, then it can be used for all these frameworks without needing to know anything about any of them.
e.g. instead of:
import component, {useState} from "neverland" //or "react" or "haunted" or "swiss"
export const myAwesomeHook = component(() => {
/*stuff with useState */
})
It'd be like (untested, just a first thought):
export interface IDeps {
useState: <S>(initialState:S) => [S, (newState:S) => void];
component?: (fn:() => any) => (() => any)
}
export const myAwesomeHook = (deps:IDeps) => {
const {useState, component} = deps;
const _hook = () => {
/*stuff with useState */
}
return component ? component(_hook) : _hook;
}
Then, as the _app developer_ - say you import it and use it like:
//in hooks.js/ts
//this is _slightly_ annoying but really not a big deal at all and could be easily automated
import {myAwesomeHook as _myAwesomeHook} from "thirdParty";
import * as HookDeps from "neverland"; //or react, haunted, swiss, etc.
export const myAwesomeHook = _myAwesomeHook(HookDeps);
//in App
import {myAwesomeHook} from "./hooks"; //notably - not from the third-party lib here
Just a thought... but it feels weird that there's all these similarities and potential for generic composition, and it doesn't even relate to semver (i.e. the odds of React changing useState is pretty different than changing other parts of the framework that would require a major bump)
I don't think I'm going to do anything about this, and there's nuance to iron out (i.e. how to differentiate between virtual and component for haunted)... but it's food for thought!
Basically, "higher order frameworks" or "higher order dependencies" (are either of those real terms?)
This is an old discussion and is unrelated to Hooks per se. You can apply the same to components — “why do they need to depend on React?” And then ask people to pass React to a wrapper function.
In practice it makes for an annoying API and doesn’t buy much. You’re still relying on semantics chosen by React — whether for components or Hooks. That’s what importing from react package is meant to express. It doesn’t hardcode any particular implementation. (There’s a reason the same Hooks will work in ReactDOM, React Native, or React DOM Server — the react package just delegates to an overridable dispatcher. It’s not currently a public API but could possibly become one if there is enough interest.)
I suggest to look at Preact ecosystem. Barely anyone there creates such “factories”. They consume components written for React by aliasing it at the module level. Passing in a factory wouldn't do anything to solve the real issues anyway (such as Preact failing to adhere to React semantics in some cases).
This point has also been discussed in depth in https://github.com/reactjs/rfcs/pull/68#issuecomment-439314884 ("The Injection Model").
Overall — you can feel free to write code this way, but it’s probably not very practical and doesn’t give you much extra flexibility. People who don't want to use react can do module aliasing. And even without module aliasing, you can redirect the dispatcher to your own implementation if you desire. The react name means that you agree with React’s semantics of what these Hooks are supposed to mean. You need to define that somehow anyway because even your “generic” Hooks would be unusable in an environment that has a different vision for what they mean.
Judging by this pattern being extremely rare in third party components (despite similar heated past discussions) I'm inclined to say it's not worth the effort.
It's sad to hear previous discussions got heated :\ I assume you don't mean fiery with the passion of helping eachother out and lifting everyone up. Sucks when it gets to the point of making something fun and exciting into an emotional drain. Been there and it's not at all where I want to take this topic.
Do you mind explaining how "module aliasing" works or pointing out a good article? I tried googling and didn't see anything (other than for bundlers like to get around relative paths). Sounds appealing - i.e. if I can build a library against React but then a consuming app can swap that out for Neverland/Haunted/Swiss/Preact/Whatever - sounds like it's half the battle and I totally agree that it's _much_ better than a dependency injection!
I do think the other half of the battle, however, is having a public API defined on some sort of type system level. It can (and should) be completely decided by the React team - and for everyone else it's either follow it or tough cookies. But I think having something to code against with, what's the term, "denotational semantics" would be great.
If I write a library that builds on useState, and I use Typescript or Flow - or even Purescript - to ensure that it works with the agreed-upon signature, then I know I can potentially compose it with other hook-library authors who also build on useState. Even if one of us is primarily building it for using in React and another in something else.
Tips on how to go the module aliasing route, plus an official doc from React on the semantics, would be wonderful.
Do you mind explaining how "module aliasing" works or pointing out a good article?
Here is an example of how Preact does it: https://preactjs.com/guide/switching-to-preact
It can (and should) be completely decided by the React team - and for everyone else it's either follow it or tough cookies.
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.
Thanks for the link to Preact's docs on aliasing! Looks very straightforward and I was not aware of that possibility. I should have realized a path is a path and aliasing package names will work just as well. Excellent!
So it sounds like it's safe as can be for hook-composition-library authors to build against React now, and plan at some point to depend on and alias the react-hooks alone once all the rough edges are sorted out (i.e. not before it comes out of alpha/beta). Makes sense and I'd imagine that the transition, if it becomes popular, will be fairly painless. Great!
Anecdotally, I've been helping out a bit with the hyperHTML version ("Neverland"), just writing some test cases and filing issues- and there were several times that the author wanted to understand more about how React did things from the user perspective. In fact the only time it came up to maybe drift away was not for hooks but for React.memo() (which, as you pointed out, isn't exactly a 1:1 correspondence to useMemo, so it's a different situation). Tbh I hadn't used hooks yet and had to dig in... so even with good intentions it can be a bit difficult to try and match it.
I realize that there's a strong effort on DefinitelyTyped and that might be a good reference for hook-composition-library authors too... (i.e. to code against types without needing to know the internals of how setState relates to memory cells, etc.)
Most helpful comment
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,useContextanduseReducer(oruseStatewhich is justuseReducerwith 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
useCallbackfor 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.