Hooks

The core hooks for building resources in tap.

tap hooks work like React hooks, but start with tap instead of use. The same rules of hooks apply.

Rules of hooks

Call hooks at the top level of a resource body

Call hooks at the top level of a custom tap* function

Don't call hooks in conditions, loops, or nested functions

Don't call hooks in event handlers or try/catch/finally

Don't call hooks in functions passed to tapState, tapMemo, or tapEffect

tapState

Local state within a resource.

const [count, setCount] = tapState(0);

// with lazy initializer
const [data, setData] = tapState(() => expensiveComputation());

// set directly
setCount(5);

// or with an updater function
setCount((prev) => prev + 1);

Unlike React, calling setCount during render throws an error. Use tapReducerWithDerivedState for adjusting state in response to prop changes.

tapReducer

Manage complex state with a reducer function. Identical to React's useReducer.

type Action = { type: "increment" } | { type: "decrement" };

const [count, dispatch] = tapReducer(
  (state: number, action: Action) => {
    switch (action.type) {
      case "increment": return state + 1;
      case "decrement": return state - 1;
    }
  },
  0,
);

dispatch({ type: "increment" });

Unlike React, calling dispatch during render throws an error. Use tapReducerWithDerivedState for adjusting state in response to prop changes.

With a lazy initializer:

const [state, dispatch] = tapReducer(reducer, initialArg, (arg) => createInitialState(arg));

tapReducerWithDerivedState

This API is experimental, intended for advanced use cases, and may change.

Like tapReducer, but with a getDerivedState function that runs during render after processing queued actions. This serves the same use case as React's getDerivedStateFromProps — adjusting internal state in response to changing props without needing to call set/dispatch during render (which tap does not support).

If getDerivedState returns a new value (by Object.is), the derived value becomes the current state and triggers a re-render.

type State = { items: string[]; selectedId: string | null; prevItems: string[] };
type Action = { type: "select"; id: string };

const [state, dispatch] = tapReducerWithDerivedState(
  (state: State, action: Action): State => {
    switch (action.type) {
      case "select":
        return { ...state, selectedId: action.id };
    }
  },
  // reset selection when items change (getDerivedStateFromProps pattern)
  (state) => {
    if (state.prevItems === items) return state;
    return { ...state, items, prevItems: items, selectedId: null };
  },
  { items, selectedId: null, prevItems: items },
);

With a lazy initializer:

const [state, dispatch] = tapReducerWithDerivedState(
  reducer,
  getDerivedState,
  initialArg,
  (arg) => createInitialState(arg),
);

tapEffect

Side effects with automatic cleanup.

tapEffect(() => {
  const ws = new WebSocket("ws://localhost:8080");

  ws.onmessage = (event) => {
    setMessages((prev) => [...prev, event.data]);
  };

  return () => ws.close();
}, []);

Without a dependency array, the effect runs after every render.

tapEffect(() => {
  console.log("rendered");
});

tapMemo

Memoize expensive computations.

const sorted = tapMemo(() => {
  return items.toSorted((a, b) => a.name.localeCompare(b.name));
}, [items]);

tapCallback

Memoize a callback.

const handleClick = tapCallback(() => {
  setCount((c) => c + 1);
}, []);

tapRef

A mutable reference that persists across renders.

const ref = tapRef(0);
ref.current = 42;

// without initial value
const ref = tapRef<HTMLElement>();

tapConst

A value computed once on mount that never changes. Useful for creating stable instances.

const id = tapConst(() => crypto.randomUUID(), []);
const emitter = tapConst(() => new EventEmitter(), []);

The second parameter must always be an empty array []. It exists so that linters can validate that no dependencies are accidentally captured.

tapEffectEvent

A stable function reference that always calls the latest closure. Useful for event handlers passed to effects.

const onMessage = tapEffectEvent((msg: string) => {
  // always has access to latest state
  setMessages((prev) => [...prev, msg]);
});

tapEffect(() => {
  socket.addEventListener("message", onMessage);
  return () => socket.removeEventListener("message", onMessage);
}, [socket]);

Composition hooks

Resources can render other resources. These hooks manage child resource lifecycles — mounting, updating, and cleaning up automatically.

  • tapResource — render a single child resource
  • tapResources — render a keyed list of child resources
  • tapResourceRoot — wrap a child resource with getValue/subscribe

Comparison with React

ReacttapBehavior
useStatetapStateIdentical
useReducertapReducerIdentical
tapReducerWithDerivedStateReducer with derived state
useEffecttapEffectIdentical
useMemotapMemoIdentical
useCallbacktapCallbackIdentical
useReftapRefIdentical
useEffectEventtapEffectEventIdentical
tapConstComputed once, never changes