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 resourcetapResources— render a keyed list of child resourcestapResourceRoot— wrap a child resource withgetValue/subscribe
Comparison with React
| React | tap | Behavior |
|---|---|---|
useState | tapState | Identical |
useReducer | tapReducer | Identical |
| — | tapReducerWithDerivedState | Reducer with derived state |
useEffect | tapEffect | Identical |
useMemo | tapMemo | Identical |
useCallback | tapCallback | Identical |
useRef | tapRef | Identical |
useEffectEvent | tapEffectEvent | Identical |
| — | tapConst | Computed once, never changes |