stable-hooks

hooks that wrap unstable values for more control over incoming hook dependencies

MIT License

Downloads
8
Stars
6
Committers
1

@ricokahler/stable-hooks

This library is well-tested but the README/docs still needs work. A 1.0 should be around the corner.

hooks that wrap unstable values for more control over incoming hook dependencies

Installation

npm i --save @ricokahler/stable-hooks

Motivation

In complex React components, it quickly becomes challenging to control how incoming values affect your downstream hooks.

For example, the following <Dialog /> component has a bug that causes it to re-run the onOpen or onClose callbacks if the consumer does not wrap the callbacks in useCallback.

import { useState, useEffect } from 'react';

function Dialog({ onOpen, onClose }) {
  const [open, setOpen] = useState(false);

  useEffect(() => {
    if (open) onOpen();
    else onClose();
  }, [open, onOpen, onClose]);

  return (
    <>
      <button onClick={() => setOpen(!open)}>Toggle Dialog</button>

      <dialog open={open}>
        <p>Greetings, one and all!</p>
      </dialog>
    </>
  );
}

export default function App() {
  const [clicks, setClicks] = useState(0);

  return (
    <>
      <button onClick={() => setClicks(clicks + 1)}>Clicks {clicks}</button>
      <Dialog
        onOpen={() => console.log('Dialog was opened!')}
        onClose={() => console.log('Dialog was closed!')}
      />
    </>
  );
}

stable-hooks are hooks you can use to wrap unstable values for more control over incoming hook dependencies.

Usage

useStableGetter

implementation | tests

Wraps incoming values in a stable getter function that returns the latest value.

Useful tool for signifying a value should not be considered as a reactive dependency.

**** When this getter is invoked, it pulls the latest value from a hidden ref. This ref is synced with the current inside of a useLayoutEffect so that it runs before other useEffects.

import { useState, useEffect } from 'react';
import { useStableGetter } from '@ricokahler/stable-hooks';

function Dialog(props) {
  const [open, setOpen] = useState(false);

  const getOnOpen = useStableGetter(props.onOpen);
  const getOnClose = useStableGetter(props.onClose);

  useEffect(() => {
    const onOpen = getOnOpen();
    const onClose = getOnClose();
    if (open) onOpen();
    else onClose();
  }, [open, getOnOpen, getOnClose]);

  return (
    <>
      <button onClick={() => setOpen(!open)}>Toggle Dialog</button>

      <dialog open={open}>
        <p>Greetings, one and all!</p>
      </dialog>
    </>
  );
}

useStableCallback

implementation | tests

Returns a stable callback that does not change between re-renders.

**** The implementation uses useStableGetter to get latest version of the callback (and the values closed within it) so values are not stale between different invocations.

import { useState, useEffect } from 'react';
import { useStableCallback } from '@ricokahler/stable-hooks';

function Dialog(props) {
  const [open, setOpen] = useState(false);

  const onOpen = useStableCallback(props.onOpen);
  const onClose = useStableCallback(props.onClose);

  useEffect(() => {
    if (open) onOpen();
    else onClose();
  }, [open, onOpen, onClose]);

  return (
    <>
      <button onClick={() => setOpen(!open)}>Toggle Dialog</button>

      <dialog open={open}>
        <p>Greetings, one and all!</p>
      </dialog>
    </>
  );
}

useStableValue

implementation | tests

Given an unstable value, useStableValue hashes the incoming value against a hashFn (by default, this is JSON.stringify) and if the hash is unchanged, the previous value will be returned.

Useful for defensively programming against unstable objects coming from props.

**** The implementation runs the value through the provided hash function and the result of that hash function is used as the only dependency in a useMemo call. See the implementation here.

function Example(props) {
  const style = useStableValue(props.style);

  useEffect(() => {
    // do something only when the _contents_ of
    // the style object changes
  }, [style]);

  return <>{/* ... */}</>;
}
Package Rankings
Top 21.59% on Npmjs.org
Badges
Extracted from project README
bundlephobia https://github.com/ricokahler/stable-hooks/actions codecov