SkillAgentSearch skills...

Rko

A state manager with undo, redo and persistence.

Install / Use

/learn @steveruizok/Rko
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

logo

Out of nowhere! A state management library for React with built-in undo, redo, and persistence. Built on Zustand.

logo

🧑‍💻 Check out the example project.

💜 Like this? Consider becoming a sponsor.

Table of Contents

Installation

npm install rko

or

yarn add rko

Usage

🧑‍🏫 Using TypeScript? See here for additional docs.

To use the library, first define your state as a class that extends StateManager. In your methods, you can use the StateManager's internal API to update the state.

// state.js
import { StateManager } from "rko"

class MyState extends StateManager {
  adjustCount = (n) =>
    this.setState({
      before: {
        count: this.state.count,
      },
      after: {
        count: this.state.count + n,
      },
    })
}

Next, export an instance of the state. If you want to persist the state, give it an id.

export const myState = new MyState({ count: 0 }, "my-state")

In your React components, you can use the state's useStore hook to select out the data you need. For more on the useStore hook, see zustand's documentation.

// app.jsx
import { myState } from "./state"

function App() {
  const { count } = myState.useStore((s) => s.count)
  return (
    <div>
      <h1>{count}</h1>
    </div>
  )
}

You can also call your state's methods from your React components.

function App() {
  const { count } = myState.useStore((s) => s.count)

  function increment() {
    myState.adjustCount(1)
  }

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={increment}>Increment</button>
    </div>
  )
}

...and you can use the StateManager's built-in methods too.

function App() {
  const { count } = myState.useStore((s) => s.count)

  function increment() {
    myState.adjustCount(1)
  }

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={increment}>Increment</button>
      <button onClick={myState.undo}>Undo</button>
      <button onClick={myState.redo}>Redo</button>
    </div>
  )
}

Right on, you've got your global state.

StateManager

The rko library exports a class named StateManager that you can extend to create a global state for your app. The methods you add to the class can access the StateManager's internal API.

import { StateManager } from "rko"

class AppState extends StateManager {
  // your methods here
}

You only need to create one instance of your StateManager sub-class. When you create the instance, pass an initial state object to its constructor.

const initialState = {
  // ...
}

export const appState = new AppState(initialState)

You can also use the constructor to:

Internal API

You can use StateManager's internal API to update your state from within your your sub-class methods.

patchState(patch: Patch<State>, id?: string)

Update the state without effecting the undo/redo stack. This method accepts a Patch type object, or a "deep partial" of the state object containing only the changes that you wish to make.

toggleMenuOpen = () =>
  this.patchState({
    ui: {
      menuOpen: !this.state.ui.menuOpen,
    },
  })

You can pass an id as setState's second parameter. This is provided to help with logging and debugging. The id will be saved in the history stack and be available in the onStateWillChange and onStateDidChange callbacks.

For example, this method:

 addMessage(newMessage) {
    this.patchState({ message: newMessage }, "added_message")
  }

Would cause onStateDidChange to receive added_message as its second argument.

setState(command: Command<State>, id?: string)

Update the state, push the command to the undo/redo stack, and persist the new state. This method accepts a Command type object containing two Patches: before and after. The after patch should contain the changes to the state that you wish to make immediately and when the command is "re-done". The before patch should contain the changes to make when the command is "undone".

adjustCount = (n) =>
  this.setState({
    before: {
      count: this.state.count,
    },
    after: {
      count: this.state.count + n,
    },
  })

Like patchState, you can provide an id as the method's second argument. Alternatively, you can provide the id as part of the command object. If you provide both, then the argument id will be used instead.

replaceState(state: State, id?: string)

Works like patchState but accepts an entire state instead of a patch. This is useful for cases where a deep merge may be too expensive, such as changing items during a drag or scroll interaction. Note that, like patchState, this method will not effect the undo/redo stack. You might also want to call resetHistory.

loadNewTodos = (state: State) =>
  this.replaceState({
    todos,
  })

cleanup(next: State, prev: State, patch: Patch<State>)

The cleanup method is called on every state change, after applying the current patch. It receives the next state, the previous state, and the patch that was just applied. It returns the "final" updated state.

cleanup = (next: State) => {
  const final = { ...state }

  for (const id in todos) {
    if (todos[id] === "undefined") {
      delete final.todos[id]
    }
  }

  return final
}

You can override this method in order to clean up any state that is no longer needed. Note that the changes won't be present in the undo/redo stack.

You can also override this method to log changes or implement middleware (see Using Middleware).

ready

The ready Promise will resolve after the state finishes loading persisted data, if any.

const state = new Example()
const message = await state.ready
// message = 'none' | 'migrated' | 'restored'

onReady()

The onReady method is called when the state is finished loading persisted data, if any.

class Example extends StateManager {
  onReady() {
    console.log("loaded state from indexdb", this.state)
  }
}

onPatch(state: State, id?: string)

The onPatch method is called after the state is changed from an onPatch call.

onCommand(state: State, id?: string)

The onCommand method is called after the state is changed from an onCommand call.

onPersist(state: State, id?: string)

The onPersist method is called when the state would be persisted to storage. This method is called even if the state is not actually persisted, e.g. an id is not provided.

onReplace(state: State)

The onReplace method is called after a call to replaceState.

onReset(state: State)

The onReset method is called after a call to resetState.

onResetHistory(state: State)

The onResetHistory method is called after a call to resetHistory.

onStateWillChange(state: State, id?: string)

The onStateWillChange method is called just before each state change. It runs after cleanup. Your React components will not have updated when this method runs.

onStateWillChange = (state: State, id: string) => {
  console.log("Changing from", this.state, "to", state, "because", id)
  // > Changed from {...} to {...} because command:toggled_todo
}

Its first argument is the next state. (You can still access the current state as this.state). The id argument will be either patch, command, undo, redo, or reset.

You can override this method to log changes or implement middleware (see Using Middleware). If you're interested in what changed, consider using the cleanup method instead.

onStateDidChange(state: State, id?: string)

The onStateDidChange method works just like onStateWillChange, except that it runs after the state has updated. Your React components will have updated by the time this method runs.

onStateDidChange = (state: State, id: string) => {
  console.log("Changed to", state, "because", id)
  // > Changed to {...} because command:toggled_todo
}

snapshot

The most recently saved snapshot, or else the initial state if setSnapshot has not yet been called. You can use the snapshot to restore earlier parts of the state (see Using Snapshots). Readonly.

Public API

The StateManager class exposes a public API that you can use to interact with your state either from within your class methods or from anywhere in your application.

undo()

Move backward in history, un-doing the most recent change.

redo()

Move forward in history, re-doing the previous undone change.

reset()

Reset the state to its initial state (as provided in the constructor). This is not undoable. Calling reset() will also reset the history.

replaceHistory(stack: Command[], pointer?: number)

Replace the state's history. By default the pointer will be set to the end of the stack. Note that it's your responsibility to ensure that the new history stack is compatible with the current state!

resetHistory()

Reset the state's history.

forceUpdate()

Force the state to update.

setSnapshot()

Save the current state to the the snapshot property (see [Using Snapshots](#

View on GitHub
GitHub Stars213
CategoryDevelopment
Updated11d ago
Forks2

Languages

TypeScript

Security Score

95/100

Audited on Mar 17, 2026

No findings