State where a hook can’t go.
Local state and the methods that update it, placed at
any point in JSX — including inside
.map() and conditionals, where a hook call is illegal.
- 0.58 kB gzip
- TypeScript
- React 18+
- MIT
{tasks.map((task) => (
<Component key={task.id} initial={{ open: false }}
actions={(self) => ({ toggle: () => self.set({ open: !self.state.open }) })}
>
{({ state, actions }) => (
<Task task={task} open={state.open} onToggle={actions.toggle} />
)}
</Component>
))}
Result
Open one row. Each element owns its state; its siblings never hear about it.
Why it exists
Hooks can only be called at the top of a component, so a list row that needs its own state forces you to invent a component to hold it — along with a props interface to thread data back into it.
tasks.map((task) => {
const [open, setOpen] = useState(false)
// React: hooks can't run in a loop
return <Task open={open} />
})
// a component that exists only to hold one boolean
function TaskRow({ task }) {
const [open, setOpen] = useState(false)
return <Task task={task} open={open} />
}
tasks.map((t) => <TaskRow key={t.id} task={t} />)
<Component> is an element, not a hook call, so it is legal
exactly where the first example is not. The row above keeps its state and
loses the wrapper.
Install
pnpm add use-component
Usage
State and behavior are grouped the way a class groups fields and methods.
The actions factory receives self — the
explicit this.
| In this library | Class equivalent |
|---|---|
| state | fields |
| actions | methods |
| self.set | this.setState — shallow partial merge |
| self.state | this.state — always the latest commit |
useComponent
The hook form, for when you are already inside a component.
import { useComponent } from 'use-component'
function Counter() {
const { state, set, actions } = useComponent({
initial: { count: 0 },
actions: (self) => ({
inc: () => self.set({ count: self.state.count + 1 }),
reset: () => self.set({ count: 0 }),
}),
})
return (
<div>
<button onClick={actions.inc}>Count: {state.count}</button>
<button onClick={actions.reset}>Reset</button>
{/* set handles one-offs that don't need a named action */}
<button onClick={() => set({ count: 100 })}>Set 100</button>
</div>
)
}
Composing actions
One action calls another through a shared local reference. The functional updater is what makes two increments accumulate within a single tick.
actions: (self) => {
const inc = () => self.set((prev) => ({ count: prev.count + 1 }))
return { inc, double: () => { inc(); inc() } } // +2
}
Loading on mount
onMount runs after the first commit and never again. Return a
function to clean up on unmount.
<Component
initial={{ user: null }}
onMount={async ({ set }) => set({ user: await fetchUser() })}
>
{({ state }) => (state.user ? <Profile user={state.user} /> : <Spinner />)}
</Component>
API
Options
Shared by useComponent and <Component>. Every field is read on the first render only.
| Option | Type | Description |
|---|---|---|
| initial | S | (() => S) |
State for the first render. Pass a function to build it lazily. |
| actions | (self) => A |
Factory for the methods, called once. They keep a stable identity afterwards. |
| onMount | (self) => void | cleanup |
Runs after the first commit. StrictMode invokes it twice in development, so undo whatever it starts in the cleanup. |
Returns
| Name | Description |
|---|---|
| state | State as of this render. |
| set | Merges a patch into state. Accepts an object or (prev) => patch. |
| actions | The methods from the factory. Stable across renders. |
Utility hooks
| Hook | Description |
|---|---|
| usePrevious(value) | The value this component saw on its previous render; undefined on the first. |
| usePreviousDistinct(value, compare?) | The last value that differed. compare returns true for same. |
| useForceUpdate() | Re-renders the calling component on demand, for values that live outside React. |