クイックスタート

インストール

npm
pnpm
yarn
bun
npm install @violetflux/kerros

Kerros は npm、pnpm、Yarn、Bun で利用でき、React 17、18、19 をサポートします。

Store を作成する

任意の custom Hook を createStore でラップすると、コンポーネント間で状態を共有できます。

import { createStore } from '@violetflux/kerros'
import { useState } from 'react'

interface Task {
  id: string
  title: string
}

function useTaskStoreValue() {
  const [tasks, setTasks] = useState<Task[]>([])

  const addTask = (task: Task) => {
    setTasks(v => [...v, task])
  }

  const finishTask = (taskId: string) => {
    setTasks(v => v.filter(task => task.id !== taskId))
  }

  return { tasks, addTask, finishTask }
}

export const [useTask, TaskProvider] = createStore(useTaskStoreValue)

createStore は、コンポーネントが使う Hook と対応する Provider を返します。Store の中では useStateuseReducer、Context、SDK Hook、独自 Hook をそのまま使えます。

initializer は useTaskStoreValue のようなトップレベルの名前付き Hook にしてください。匿名 initializer も動作しますが、React Compiler の infer モードでは Hook として自動コンパイルされません。

Provider を配置する

useTaskTaskProvider の子孫でのみ呼び出せます。

function App() {
  return (
    <TaskProvider>
      <Header />
      <TaskList />
    </TaskProvider>
  )
}

Provider の外で呼び出すと、Kerros は明確なエラーを投げます。

コンポーネントで Store を使う

selector から、コンポーネントが使うフィールドだけを返します。

function TaskList() {
  const { tasks, finishTask } = useTask(s => ({
    tasks: s.tasks,
    finishTask: s.finishTask,
  }))

  return (
    <ul>
      {tasks.map(task => (
        <li key={task.id}>
          {task.title}
          <button onClick={() => finishTask(task.id)}>完了</button>
        </li>
      ))}
    </ul>
  )
}

Kerros は selector オブジェクトのトップレベルを浅く比較します。選択したフィールドが同じなら、Store の他の更新で TaskList は再レンダーされません。selector はインラインでよく、useCallback は不要です。

複数の Store インスタンス

Provider を配置するたびに、独立した Store が作られます。

<TaskProvider>
  <h2>個人タスク</h2>
  <TaskList />
</TaskProvider>

<TaskProvider>
  <h2>チームタスク</h2>
  <TaskList />
</TaskProvider>

TaskList は最も近い Provider を自動的に読み、データは互いに影響しません。

Store 間の依存

ある Store から別の Store を直接呼び出せます。

function useTaskStoreValue() {
  const { user } = useAccount(s => ({ user: s.user }))
  const [tasks, setTasks] = useState<Task[]>([])

  const addTask = (title: string) => {
    if (!user)
      return

    setTasks(v => [...v, {
      id: crypto.randomUUID(),
      title,
      assigneeId: user.id,
    }])
  }

  return { tasks, addTask }
}

export const [useTask, TaskProvider] = createStore(useTaskStoreValue)

依存される Provider を外側に配置し、依存関係を一方向に保ちます。

<AccountProvider>
  <TaskProvider>
    <App />
  </TaskProvider>
</AccountProvider>

Provider に props を渡す

Provider の props は Store Hook の引数になります。

function useCounterStoreValue({ initialCount }: { initialCount: number }) {
  const [count, setCount] = useState(initialCount)
  return { count, setCount }
}

const [useCounter, CounterProvider] = createStore(useCounterStoreValue)
<CounterProvider initialCount={42}>
  <Counter />
</CounterProvider>

試してみる

このカウンターも同じ createStore + Provider + selector API を使っています。

次は SelectorStore の合成 を参照してください。