0.3.0 • Published 3 months ago

@susisu/hookshelf v0.3.0

Weekly downloads
103
License
MIT
Repository
github
Last release
3 months ago

Hookshelf

CI

Hookshelf provides React hooks through context.

# npm
npm i @susisu/hookshelf
# yarn
yarn add @susisu/hookshelf
# pnpm
pnpm add @susisu/hookshelf

If we think React hooks as "effects", there are only a fixed number of effects types (i.e. build-in hooks), and they are all handled by React. By providing hooks through context, we can extend the number of effect types to built-in hooks + custom hooks, and they can be handled by a user.

Usage

One typical usage is mocking hooks for testing.

Suppose we have a Hook like below:

import { hooks } from "./lib";

const { useBrowserFeature, useNetworkFetch, useComplexState } = hooks;

export function useMyHook() {
  const id = useBrowserFeature();
  const { data, error } = useNetworkFetch(id);
  const { state, dispatch } = useComplexState();
  // do something with data, error, state, and dispatch
  return ...;
}

Testing this hooks will be a hard work, because we first need to mock browser features and network to let useBrowserFeature and useNetworkFetch work in the test, and then we need to wait for data, change state, etc. before asserting results.

test("It works", () => {
  ... // Mock browser features and network

  const { result } = renderHook(useMyHook);
  expect(result.current).toEqual(...);

  ... // Wait for data
  expect(result.current).toEqual(...);

  ... // Change state
  expect(result.current).toEqual(...);

  ... // And change state again, assert, ...
});

One way to solve this issue is separating our Hook into two parts: complex one and simple one. By doing this, we can easily test the latter part, however, we still have difficulties testing, for example, the id retrieved from useBrowserFeature is passed to useNetworkFetch. Another way is using mocking facilities that the testing framework provides. This is good for some situations, but we should use it moderately because it has global effect and usually not typesafe.

Hookshelf provides a third way by providing hooks through React context.

First, create a React context using createHookshelf:

import { createHookshelf } from "@susisu/hookshelf";
import { hooks } from "./lib";

export const [HooksProvider, proxyHooks] = createHookshelf(hooks);

Now we have a provider component which provides hooks to the context, and proxy hooks which invoke corresponding hooks in the context.

Next, replace hook invocations to proxy hooks:

import { proxyHooks } from "./shelf";

const { useBrowserFeature, useNetworkFetch, useComplexState } = proxyHooks;

export function useMyHook() {
  const id = useBrowserFeature();
  const { data, error } = useNetworkFetch(id);
  const { state, dispatch } = useComplexState();
  // do something with data, error, state, and dispatch
  return ...;
}

The proxy hooks will invoke the original hooks if we don't use the provider component, so this will not change the behavior of our hook.

Mocking the hooks in tests can be easily done by providing fake hooks using the provider component:

import { HooksProvider } from "./shelf";

function prepare({ id, data, error, state, dispatch }) {
  const hooks = {
    useBrowserFeature: jest.fn(() => id),
    useNetworkFetch: jest.fn(() => ({ data, error })),
    useComplexState: jest.fn(() => ({ state, dispatch })),
  };
  const Wrapper = ({ children }) => (
    <HooksProvider hooks={hooks}>{children}</HooksProvider>
  );
  return { hooks, Wrapper };
}

test("It returns some data created from fetched data and state", () => {
  const { hooks, Wrapper } = prepare({
    id: 42,
    data: { ... },
    error: undefined,
    state: { ... },
    dispatch: () => {},
  });
  const { result } = renderHook(useMyHook, { wrapper: Wrapper });
  expect(result.current).toEqual(...);
});

test("It returns another data in another state", () => {
  const { hooks, Wrapper } = prepare({
    id: 42,
    data: { ... },
    error: undefined,
    state: { ... },
    dispatch: () => {},
  });
  const { result } = renderHook(useMyHook, { wrapper: Wrapper });
  expect(result.current).toEqual(...);
});

Caveats

Use Hookshelf carefully not to break the Rules of Hooks. For example, you should not change hooks after components are rendererd.

License

MIT License

Author

Susisu (GitHub, Twitter)

0.3.0

3 months ago

0.2.1

2 years ago

0.2.0

3 years ago

0.1.0

3 years ago