React bindings

useNuskaEngine, NuskaProvider, and useNuska for reactive version control in React.

All React bindings are imported from @codecanon/nuska/react.

import { useNuskaEngine, NuskaProvider, useNuska } from "@codecanon/nuska/react"

useNuskaEngine

The primary hook for using nuska in React. Creates and owns a stable NuskaEngine instance, initializes it on mount, and returns reactive state alongside bound action methods.

import { useNuskaEngine } from "@codecanon/nuska/react"
import { MemoryDataSourceAdapter, MemoryVersionStore } from "@codecanon/nuska"

// Create adapters outside the component so they are stable
const datasource = new MemoryDataSourceAdapter()
const store = new MemoryVersionStore()

export function App() {
  const nuska = useNuskaEngine({ datasource, store })

  if (!nuska.ready) return <p>Loading…</p>

  return <p>Branch: {nuska.currentBranch}</p>
}

Options

PropTypeDescription
datasourceDataSourceAdapterYour datasource adapter
storeVersionStoreYour version store adapter
defaultBranchstringInitial branch name. Default: "main"
generateId() => stringCustom ID generator. Default: uuid v4

State

PropertyTypeDescription
readybooleantrue once init() has completed
currentBranchstringName of the checked-out branch
branchesBranch[]All branches
logCommit[]Commit history for the current branch
pullRequestsPullRequest[]All pull requests
lastDiffDiff | nullMost recent diff result, or null
conflictsConflictEntry[]Active conflicts from the last merge
pendingMerge{ sourceBranch: string; targetBranch: string; prId?: string } | nullSet when a merge is awaiting conflict resolution
engineNuskaEngineEscape hatch — the underlying engine instance

Actions

All action methods call refresh() automatically after completion so state stays in sync.

MethodSignatureDescription
refresh() => Promise<void>Re-sync all state from the engine
commit(ops, message, author) => Promise<Commit>Persist a commit
branch(name) => Promise<Branch>Create a branch
checkout(name) => Promise<void>Switch branch
diff(fromId, toId) => Promise<Diff>Compare two commits, stores result in lastDiff
deleteBranch(name) => Promise<void>Delete a branch
revertCommit(commitId, opts?) => Promise<Commit>Revert a commit
merge(sourceBranch) => Promise<MergeResult>3-way merge into current branch
resolveConflicts(resolutions) => Promise<{ commitId: string }>Finish a conflicted merge
createPR(title, fromBranch, toBranch) => Promise<PullRequest>Open a PR
mergePR(prId, options?) => Promise<MergeResult>Merge a PR
closePR(prId) => Promise<PullRequest>Close a PR
clearConflicts() => voidReset conflict state
clearDiff() => voidReset lastDiff to null

NuskaProvider + useNuska

Use NuskaProvider to share a single engine instance across a React subtree. Any component inside the tree can call useNuska() to access the same state and actions.

import { NuskaProvider, useNuska } from "@codecanon/nuska/react"
import {
  IndexedDBDataSourceAdapter,
  IndexedDBVersionStore,
} from "@codecanon/nuska/adapters"

const datasource = new IndexedDBDataSourceAdapter("my-app-data")
const store = new IndexedDBVersionStore("my-app-versions")

export function Root() {
  return (
    <NuskaProvider options={{ datasource, store }}>
      <App />
    </NuskaProvider>
  )
}

function App() {
  const { ready, currentBranch, commit } = useNuska()

  if (!ready) return <p>Loading…</p>

  return <p>Branch: {currentBranch}</p>
}

useNuska() throws if called outside a NuskaProvider.


Usage example — commit form

import { useState } from "react"
import { useNuska } from "@codecanon/nuska/react"

export function CommitForm() {
  const { commit } = useNuska()
  const [key, setKey] = useState("")
  const [value, setValue] = useState("")

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault()
    await commit(
      [{ type: "set", key, value: JSON.parse(value) }],
      `Set ${key}`,
      "user"
    )
    setKey("")
    setValue("")
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={key} onChange={(e) => setKey(e.target.value)} placeholder="key" />
      <input value={value} onChange={(e) => setValue(e.target.value)} placeholder="value (JSON)" />
      <button type="submit">Commit</button>
    </form>
  )
}

Usage example — branch selector

import { useNuska } from "@codecanon/nuska/react"

export function BranchSelector() {
  const { currentBranch, branches, checkout, branch } = useNuska()

  return (
    <div>
      <select
        value={currentBranch}
        onChange={(e) => checkout(e.target.value)}
      >
        {branches.map((b) => (
          <option key={b.name} value={b.name}>{b.name}</option>
        ))}
      </select>
      <button onClick={() => branch(prompt("New branch name") ?? "")}>
        New branch
      </button>
    </div>
  )
}

Usage example — conflict resolution

import { useNuska } from "@codecanon/nuska/react"

export function ConflictPanel() {
  const { conflicts, pendingMerge, resolveConflicts, clearConflicts } = useNuska()

  if (!pendingMerge || conflicts.length === 0) return null

  async function acceptTheirs() {
    await resolveConflicts(
      conflicts.map((c) =>
        c.theirsValue === undefined
          ? { key: c.key, deleted: true }
          : { key: c.key, value: c.theirsValue }
      )
    )
  }

  return (
    <div>
      <p>{conflicts.length} conflict(s) merging {pendingMerge.sourceBranch}</p>
      {conflicts.map((c) => (
        <div key={c.key}>
          <strong>{c.key}</strong>
          <span>Ours: {JSON.stringify(c.oursValue)}</span>
          <span>Theirs: {JSON.stringify(c.theirsValue)}</span>
        </div>
      ))}
      <button onClick={acceptTheirs}>Accept theirs</button>
      <button onClick={clearConflicts}>Cancel</button>
    </div>
  )
}

On this page