use-component

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.

TSX TaskList.tsx
{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

Tag the commit, run the build, publish to npm.
First paragraph is the hover. One sentence, no name echo.
Typecheck, test, build — on every pull request.

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.

Illegal
tasks.map((task) => {
  const [open, setOpen] = useState(false)
  // React: hooks can't run in a loop
  return <Task open={open} />
})
The workaround, today
// 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 libraryClass equivalent
statefields
actionsmethods
self.setthis.setState — shallow partial merge
self.statethis.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.

OptionTypeDescription
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

NameDescription
stateState as of this render.
setMerges a patch into state. Accepts an object or (prev) => patch.
actionsThe methods from the factory. Stable across renders.

Utility hooks

HookDescription
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.