question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Question: How should libraries export custom hooks?

See original GitHub issue

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,

  • 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:closed
  • Created 5 years ago
  • Comments:7 (3 by maintainers)

github_iconTop GitHub Comments

3reactions
Jessidhiacommented, Jan 16, 2019

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.

1reaction
gaearoncommented, Jan 17, 2019

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.

Read more comments on GitHub >

github_iconTop Results From Across the Web

How to Build Your Own React Hooks: A Step-by-Step Guide
It exports a function, which we will call copy . Next we will create a function that will be used for copying whatever...
Read more >
Implementing React Custom Hooks: A Complete Guide
Custom Hooks are an excellent way to share logic between components and improve your component tree's readability.
Read more >
Hooking the hooks - Medium
I've created a useHooks hook and imported both ahooks and my own useOnline custom hook. The only thing my hook does is spreading...
Read more >
How do I use React hooks in my component library?
I also tried reverting everything back to vanilla JS and the problem persists, so I think it might be webpack related? Edit. Here...
Read more >
How to Create and Test React Custom Hooks - Bits and Pieces
A quick React Custom Hooks tutorial — with examples! ... Why do we need hooks for this, you ask? ... }export default useInput;....
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found