React Graft

Turns a hook into a context. A graft is one piece of state, its operations and its side effects, written in one file and read anywhere below its provider.

Installation

bash
npm install react-graft

The shape

tsx
export const GraftLikes = createGraft({
  name: "GraftLikes",
  graft: useGraft,
});

function useGraft({ start = 0 }: { start?: number }) {
  const [value, setValue] = useState(start);
  const like = useCallback(() => setValue((count) => count + 1), []);

  return { value, like };
}

<GraftLikes.Provider start={10}>
  <LikeButton />
</GraftLikes.Provider>

const { value, like } = GraftLikes.use();

What it adds to a context

Props typed from your hook

Whatever the hook accepts becomes a prop on the provider, inferred from its own signature; whatever it returns is what use() hands back. There is no second declaration to keep in step with the first, so composing four hooks into one, exposing three members out of twenty or overriding one with a different signature costs nothing but the object you return.

A context nobody can supply

createGraft returns Provider, use and displayName — never the context itself. Mounting the provider is the only way in, so a reader can never receive a value the hook did not produce, and use() throws naming the graft when it is called outside one, at the first render rather than through a null three components later.

A place for the effects of a feature

A feature is rarely only state and operations. Something has to keep the url in step, persist, report, listen for a shortcut. inject takes those hooks and runs them inside the provider, each receiving the value the graft produced, so they belong to the feature instead of to whichever component happened to be mounted when they were needed.

Boundaries

Plain React

No proxies, no compiler, no subscription trick underneath: a hook you wrote, called by a provider, read from a context. The rules of hooks apply and the linter enforces them, use() included. The reverse is true as well — a value that changes wakes every reader of that graft, and the fix is a narrower graft rather than a cleverer subscription.

One function, and nothing else

The package is createGraft. It renders nothing, touches no DOM and reaches for no platform API; the hooks are yours to write, and the examples show what they look like. State lives in useState and useRef where the provider is, so two providers are two independent instances and unmounting one takes its effects with it.