compose-hooks-react

Compose React hooks together and decouple them from your components

Usage no npm install needed!

<script type="module">
  import composeHooksReact from 'https://cdn.skypack.dev/compose-hooks-react';
</script>

README

React Hooks Compose

React hooks are nice. They solve some issues with functional components like using state and lifecycle methods. However, they also tend to tightly couple hooks to React components. This leads to less composability of components, more difficulty and complexity in unit testing components. This library allows you to pass your hooks to your component. It also allows your hooks to compose and by attaching what is returned from each hook to the overall props that are passed to the next hook.

Note there is also a compose.d.ts file that has typings for composeHooks so that you can use this package withing a TypeScript project without any additional dependencies for @types.

Basic Usage

The compose function takes in an arbitrary amount of React hooks or you could just use any functions you want. It calls them from left-to-right/top-to-bottom. The preferred way is to have your hooks return an object with key/value pairs, but this is not completely necessary. This way you can access the returned vales from props. However, if you return something like a single value or an array ([someState, setSomeState]) it will use the name of the hook/function as the key on props to access that value.

import React from 'react';
import composeHooks from 'react-compose-hooks';

const useFirstName = () => 'Jeremy';
const useLastName = () => ({ lastName: 'Pivens' });
const useFullName = props => ({ fullName: `Sir ${props.useFirstName} ${props.lastName}` });

export const TestComponent = ({ fullName }) => (<p>Hello, { fullName }</p>);
export const Composed = compose(
  useFirstName,
  useLastName,
  useFullName,
)(TestComponent);

export default Composed;

Notice a few things here. We can separately export our component without it being coupled to any hooks at all. As long as in the end we pass in a fullName on props that's all the component needs. We can no write unit tests very easily for this component. Maybe it uses redux and connect? Maybe it uses hooks or maybe it using something completely different. Doesn't matter. And we can export our composed version as well.

Notice that useFullName accesses props which are values computed by previous hooks. The hooks must be passed into compose in an order were the output will be piped to the next function. If we changed the above code to

export const Composed = compose(
  useFullName,
  useFirstName,
  useLastName,
)(TestComponent)

Then useFullName will not be able to access any props or values from the other hooks. Although it will still have access to any props that are passed to the component. Lastly, notice that 2 of the hooks were returning objects. This is nice because it is the same type as props for components. However, notice in useFullName we access props.useFirstName for the value that was returned by the useFirstName hook. Whenever a hook exports any value other than an Object composeHooks will try to use the Function.name as the key on the props object.

You can compose hooks that use hooks including async hooks and useState, useContext, useEffect, etc.