kanban
v0.1.14File-backed kanban board — tickets, threaded comments, agent assignment, the kanban:change and kanban:comment trigger types, and an injectable console board with agent profiles for planning and review.
Install
iii trigger compose::add worker=kanban@0.1.14Run in your project with the iii engine running. Exact versions are immutable and digest-pinned.
skill doc
react
how-toReact
Docs https://react.dev/learn · reference https://react.dev/reference/react. Read the project's installed version's docs when the two disagree — the API surface has moved (React 19 in particular).
The mental model, in three sentences
- Render is pure and repeatable. Given the same props and state it produces the same JSX; it must not call functions, mutate things, or start network work.
- State is a snapshot per render. Setting it schedules a new render; the variable you just read is the old value for the rest of this one.
- Effects synchronize with external systems — sockets, DOM APIs, subscriptions, timers. They are not the place for derived data, event responses, or initialization.
Rules that catch most bugs
- Hooks at the top level of a component or hook, unconditional. No hooks in loops, conditions,
or after an early
return. - Keys are identity.
key={index}on a list that reorders or filters corrupts state. Use a stable id from the data. - StrictMode double-invokes render and remounts effects in dev. That is a feature: it exposes missing cleanup and non-idempotent effects. Fix the code; do not remove StrictMode.
- Never mutate props or state. Copy, then set.
- Do not lie about dependencies. A stale-closure bug is a missing dep or a non-memoized function. Fix the dependency, do not silence the lint rule.
State placement
- Compute what you can; do not store it. If it is derivable from props or state, derive it during render. Mirroring creates a second source of truth that drifts.
- Colocate, then lift. Keep state in the closest component that needs it; lift only when two siblings must agree, and lift to their closest common parent, not to the top of the app.
- One state variable per concern.
useReducerwhen several fields change together or the next value depends on the previous (setCount(c => c + 1)— batching means the object form is often wrong in a burst of events). - Reset state by changing
key, not by an effect.is the correct "reset on entity change" and also resets every child. - Server data is not React state. Cache it (TanStack Query or equivalent); keeping a copy
in
useStateguarantees stale UI and double-loading. - URL is state. Filters, tabs, pagination, and selection belong in search params so they survive reload, back/forward, and sharing.
Effects
useEffect(() => {
const ac = new AbortController()
let active = true
void (async () => {
const data = await fetchThing(id, { signal: ac.signal })
if (active) setData(data)
})()
return () => { active = false; ac.abort() }
}, [id])- Every effect that starts something must clean it up. Timers, listeners, subscriptions, sockets, in-flight requests.
- Guard against races (
activeflag,AbortController) whenever the dep can change mid-flight — otherwise a slow response overwrites a fast one. useLayoutEffectonly when you must read or write layout before paint (measuring, scroll restoration). It blocks paint; preferuseEffect.- Effects are the wrong tool for: responding to a click, deriving state, fetching as a general pattern when a data layer exists, or reacting to a prop you could compute from.
Refs, context, and escape hatches
- Refs hold values that do not affect rendering (timers, DOM nodes, previous values). Do not read or write them during render — only in effects and handlers.
useImperativeHandlewhen a parent genuinely needs a handle on a child's behavior.- Context is for low-frequency values (theme, current user, the iii client). Split it: a
context whose value object is rebuilt every render re-renders every consumer. Pair a reducer
with a context and expose
stateanddispatchin two separate contexts. useSyncExternalStorefor subscribing to a store outside React (including the iii client's connection state) — it is the correct primitive, and hand-rolled subscription effects get it wrong under concurrent rendering.
Composition
childrenas a prop breaks prop-drilling without context.- Slots (
) beat boolean flags that switch internal branches into a second component. - Compound components (
,,) share state through context; the parent owns the invariant. - A component with 6+ boolean props is 2^6 configurations nobody tests. Split on the axis that actually varies.
- Hoist state to the leaf that needs it, and keep lists virtualized when they exceed a few hundred rows.
Suspense, transitions, errors
+React.lazyfor route-level code splitting; the fallback is a real UI state, not a spinner on a blank screen.useTransition/startTransitionfor updates that may be expensive (tab switches, filtered lists) so typing stays responsive.useDeferredValuewhen a value is the slow part (a query fed to a heavy list).- Error boundaries are still the mechanism for render errors — a class component or
react-error-boundary. An async rejection is not caught by a boundary unless you rethrow it in render or route it through the data layer. use()(React 19) unwraps a promise or context in render, but only inside a Suspense boundary.
React 19 notes: refs are props (, no forwardRef), is itself
usable as the provider, useActionState / useOptimistic / cover form
pending and optimistic UI, and document metadata (, ) can render inline.
Check the installed version before using any of these.
Performance: measure, do not decorate
Order of leverage:
- Do less work. Ship fewer/smaller deps, split routes, virtualize long lists, paginate.
- Stop unnecessary re-renders — but only after profiling. React DevTools' Profiler shows which component re-rendered and why.
- Then memoize.
memo,useMemo,useCallbackcost memory and comparison time; they are net-negative on cheap components with stable inputs and net-positive on expensive subtrees fed by unstable objects. - Never memoize to fix a bug. A correct dependency array is the fix;
useMemois only an optimization.
- Keep the tree shallow; a context value changing high up re-renders the world.
- Split a hot component so the state change lands low in the tree.
- Long lists: virtualize (TanStack Virtual,
react-window) — rendering 5000 rows is the cost, not the state. - Avoid layout thrash: batch DOM reads and writes; never read
getBoundingClientRect()in the same frame as a write loop.
Accessibility is part of the component
Semantic elements before ARIA, a real before a clickable Anti-patterns to refuse
useEffect(() => setX(a + b), [a, b])).useMemo everywhere as a habit.