A flexible and lightweight state management library for React applications, featuring tree and flat state support, middleware system, and powerful dev tools.
Bonsai offers a unique approach to state management:
- Simple Yet Powerful: Easy to learn, but powerful enough for complex applications
- Type-Safe by Default: Full TypeScript support with excellent type inference
- Flexible Architecture: Choose between tree state, flat state, or scoped state
- Built-in DevTools: Debug and inspect state changes in real-time
- Middleware System: Extend functionality with custom middleware
- Zero Dependencies: Lightweight and fast, with no external dependencies
- React Native Ready: Works seamlessly with both web and mobile React applications
- π³ Tree State: Manage nested state with path-based access
- πΏ Flat State: Simple key-value state management
- πͺ΄ Scoped State: Isolated state stores for components
- π Middleware: Transform or block state updates
- π οΈ DevTools: Visual debugging and state inspection
- π¦ TypeScript: Full type safety and autocompletion
- createStore API: one-call setup for tree state with options
{ devtools, middleware }. - Auto-mounted DevTools: enable with
devtools: true(no component needed). - Modern DevTools UI: tabs (State, Inspector, Logs, Settings), draggable, resizable, search, copy buttons with feedback, and a toggle hotkey.
- Hotkey: Ctrl+Shift+B toggles the DevTools panel.
- Koa-style middleware adapter: pass
(next) => (path, value, prev) => next(path, value)directly.
Note: The classic API (
initTreeState,useTreeBonsai,set,get,subscribe) is still available and supported. New projects are encouraged to usecreateStorefor simpler setup and optional DevTools auto-mounting.
- React >= 18.2.0
- TypeScript >= 5.0.0 (recommended)
# Using npm
npm install @bonsai-ts/state
# Using yarn
yarn add @bonsai-ts/state
# Using pnpm
pnpm add @bonsai-ts/state
# Using bun
bun add @bonsai-ts/stateimport { createStore } from "@bonsai-ts/state";
// Initialize once with options (auto-mounts DevTools in dev when enabled)
export const appStore = createStore(
{
count: 0,
user: { name: "elorm", isActive: true },
},
{ devtools: true }
);
// Use in components
function Counter() {
const count = appStore.use<number>("count") || 0;
return (
<button onClick={() => appStore.set("count", count + 1)}>
Increment ({count})
</button>
);
}import { initTreeState, useTreeBonsai, set } from "@bonsai-ts/state";
// Initialize tree state
initTreeState({
initialState: {
count: 0,
user: {
name: "elorm",
isActive: true,
},
},
});
// Use in components
function Counter() {
const count = useTreeBonsai("count");
const name = useTreeBonsai("user/name");
return (
<div>
<p>Count: {count}</p>
<p>Name: {name}</p>
<button onClick={() => set("count", count + 1)}>Increment</button>
</div>
);
}import { useBonsai, setState } from "@bonsai-ts/state";
function UserProfile() {
const name = useBonsai((state) => state.name || "");
const isActive = useBonsai((state) => state.isActive || false);
return (
<div>
<p>Name: {name}</p>
<button onClick={() => setState({ isActive: !isActive })}>
Toggle Status
</button>
</div>
);
}There are two ways to use DevTools:
- Recommended: enable auto-mount via
createStore(..., { devtools: true }). - Manual: render the component yourself if you want full control.
Manual render example:
import { DevPanel } from "@bonsai-ts/state/devtools";
function App() {
return (
<div>
<Counter />
<UserProfile />
<DevPanel />
</div>
);
}The DevPanel provides:
- π³ State Tree View: Visualize your entire state tree
- π Log Viewer: Track all state changes in real-time
- π State Inspector: Inspect and modify state values
- β‘ Performance Monitor: Track re-renders and updates
- β¨ Enhanced Object/Array Visualization: Structured, collapsible, and editable views for complex data types.
- β¨οΈ Hotkey: Ctrl+Shift+B to toggle visibility.
- π§ Draggable & Resizable: drag the header, resize from the bottom-right handle.
- π Copy with feedback: copy paths or JSON values with visual confirmation.
Note: The DevPanel is only included in development builds and is automatically excluded from production builds. You only need to import and use it if you want to visualize and debug your application's state.
import {
initTreeState,
useTreeBonsai,
set,
createLoggingMiddleware,
createValidationMiddleware,
} from "@bonsai-ts/state";
// Initialize with middleware
initTreeState({
initialState: {
user: {
name: "",
preferences: {
theme: "light",
notifications: true,
},
},
},
middleware: [
createLoggingMiddleware({ logPath: true, logValue: true }),
createValidationMiddleware((path, value) => {
if (
path === "user/name" &&
typeof value === "string" &&
value.length < 2
) {
return "Name must be at least 2 characters long";
}
return true;
}),
],
});
function UserProfile() {
const name = useTreeBonsai("user/name");
const theme = useTreeBonsai("user/preferences/theme");
return (
<div>
<input value={name} onChange={(e) => set("user/name", e.target.value)} />
<button
onClick={() =>
set("user/preferences/theme", theme === "light" ? "dark" : "light")
}
>
Toggle Theme
</button>
</div>
);
}import { useBonsai, setState } from "@bonsai-ts/state";
function UserProfile() {
const name = useBonsai((state) => state.name || "");
const notifications = useBonsai((state) => state.notifications || false);
return (
<div>
<input
value={name}
onChange={(e) => setState({ name: e.target.value })}
/>
<label>
<input
type="checkbox"
checked={notifications}
onChange={(e) => setState({ notifications: e.target.checked })}
/>
Enable Notifications
</label>
</div>
);
}import { createBonsaiStore } from "@bonsai-ts/state";
// Create a scoped store
const todoStatsStore = createBonsaiStore<{
totalCompleted: number;
totalPending: number;
}>();
function TodoStats() {
const stats = todoStatsStore.use((state) => state);
return (
<div>
<p>Total Completed: {stats.totalCompleted || 0}</p>
<p>Total Pending: {stats.totalPending || 0}</p>
</div>
);
}
// Update stats from tree state
subscribe("todos", (todos) => {
if (!todos) return;
const totalCompleted = todos.filter((todo) => todo.completed).length;
const totalPending = todos.length - totalCompleted;
todoStatsStore.set({ totalCompleted, totalPending });
});import { createStore, createLoggingMiddleware } from "@bonsai-ts/state";
export const counterStore = createStore(
{ count: 0 },
{
devtools: true, // auto-mounts the DevTools floating panel
middleware: [
createLoggingMiddleware({ logPath: true, logValue: true }),
// Or Koa-style middleware adapter
(next) => (path, value, prev) => {
console.log(`State change at ${path}:`, { prev, value });
return next(path, value);
},
],
}
);
// Usage in components
function Counter() {
const count = counterStore.use<number>("count");
return (
<button onClick={() => counterStore.set("count", (count || 0) + 1)}>
{count || 0}
</button>
);
}createStore(initialState, options)returns an object with:get(path): read a value at a pathset(path, value): async update; resolves totrue | falseif blocked by middlewaresubscribe(path, cb): listen for changes under a pathuse<T>(path): React hook to read and subscribe to a pathaddMiddleware(mw): add middleware after initialization
options:devtools?: booleanβ auto-mount floating DevTools panel whentruemiddleware?: (Middleware | (next) => (path, value, prev) => any)[]β chain of middleware; Koa-style functions are adapted automatically
Tips:
setis async; await if you need to read immediately after updating.- Use specific paths in
use(path)to minimize re-renders.
If you previously used initTreeState, migrating to createStore takes a few steps:
- Initialize once with
createStoreinstead ofinitTreeState:- Before:
initTreeState({ initialState, middleware: [m1, m2] });
- After:
export const appStore = createStore(initialState, { middleware: [m1, m2], devtools: true, });
- Before:
- Replace direct tree APIs in components:
useTreeBonsai(path)βappStore.use(path)set(path, value)βappStore.set(path, value)(still async)subscribe(path, cb)βappStore.subscribe(path, cb)- Optional:
appStore.addMiddleware(mw)to add later
- DevTools:
- Before: render
<DevPanel />manually - After: set
devtools: trueto auto-mount; or keep manual component render if preferred
- Before: render
- Middleware:
- Existing middleware works as-is; you can also pass Koa-style
(next) => (path, value, prev)functions
- Existing middleware works as-is; you can also pass Koa-style
- Type-safety & paths:
- Continue to use forward-slash paths like "user/name"
- Async behavior reminder:
setis async;await appStore.set(path, value)when you need the updated value immediately
Minimal example conversion:
// Before
initTreeState({ initialState: { count: 0 } });
function Counter() {
const count = useTreeBonsai("count") || 0;
return <button onClick={() => set("count", count + 1)}>{count}</button>;
}
// After
export const store = createStore({ count: 0 }, { devtools: true });
function Counter() {
const count = store.use<number>("count") || 0;
return <button onClick={() => store.set("count", count + 1)}>{count}</button>;
}- How do I enable DevTools? Use
createStore(..., { devtools: true }). Or render<DevPanel />manually from@bonsai-ts/state/devtools. - How do I disable DevTools in production? Auto-mount is only intended for development; avoid passing
devtools: truein production builds. The component is tree-shakeable when not imported. - Does this work with Next.js/SSR? Yes. The store APIs are isomorphic; the DevTools auto-mount only runs in the browser (checks
document). - Is
setasync now? Yes. Awaitsetif you need to read the updated value immediately after. - Can I add middleware after creating the store? Yes:
store.addMiddleware(mw). - React Native? Supported. DevTools auto-mount is web-only; for RN, omit
devtools: trueor build a separate debug screen using the store APIs. - Hotkey conflicts? The DevTools toggle uses Ctrl+Shift+B. You can collapse via the close button if thereβs a conflict.
-
State Updates Not Reflecting
// β Wrong set("user/name", "John"); console.log(get("user/name")); // Might not show updated value (set is async) // β Correct await set("user/name", "John"); console.log(get("user/name")); // Will show updated value
-
Middleware Not Working
// β Wrong initTreeState({ initialState: { count: 0 }, middleware: myMiddleware, // Should be an array }); // β Correct initTreeState({ initialState: { count: 0 }, middleware: [myMiddleware], });
-
Type Errors with Paths
// β Wrong useTreeBonsai("user.name"); // Using dot notation // β Correct useTreeBonsai("user/name"); // Using forward slash
-
Use Scoped State for Local State
// For component-specific state const localStore = createBonsaiStore({ count: 0 }); const count = localStore.use((state) => state.count);
-
Optimize Re-renders
// Use specific paths instead of entire objects const name = useTreeBonsai("user/name"); // β const user = useTreeBonsai("user"); // β
-
Batch Updates
// Multiple updates in one render set("user", { name: "John", age: 30, });
import {
initTreeState,
createValidationMiddleware,
createLoggingMiddleware,
createPersistenceMiddleware,
} from "@bonsai-ts/state";
// Validation middleware
const positiveNumberValidator = createValidationMiddleware<number>(
(path, nextValue) => {
if (typeof nextValue !== "number") {
return "Value must be a number";
}
if (nextValue < 0) {
return "Value must be positive";
}
return true;
}
);
// Logging middleware
const logger = createLoggingMiddleware<number>({
logPath: true,
logValue: true,
logPrevValue: true,
});
// Persistence middleware
const persister = createPersistenceMiddleware<number>("counter");
// Initialize with middleware chain
initTreeState({
initialState: { counter: 0 },
middleware: [positiveNumberValidator, logger, persister],
});import {
initTreeState,
createAsyncMiddleware,
createDebounceMiddleware,
} from "@bonsai-ts/state";
// Async middleware for API calls
const apiMiddleware = createAsyncMiddleware<number>(async (path, nextValue) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log(`[API] Updating ${path} to ${nextValue}`);
return nextValue;
});
// Debounce middleware
const debouncer = createDebounceMiddleware<number>(500);
initTreeState({
initialState: { searchQuery: "" },
middleware: [debouncer, apiMiddleware],
});import {
initTreeState,
createThrottleMiddleware,
createTimeWindowMiddleware,
} from "@bonsai-ts/state";
// Throttle middleware (1 update per second)
const throttler = createThrottleMiddleware<number>(1);
// Time window middleware (business hours only)
const timeWindow = createTimeWindowMiddleware<number>([
9, 10, 11, 12, 13, 14, 15, 16, 17,
]);
initTreeState({
initialState: { apiCalls: 0 },
middleware: [throttler, timeWindow],
});import {
initTreeState,
createValidationMiddleware,
createAsyncMiddleware,
} from "@bonsai-ts/state";
// User data validation
const userValidator = createValidationMiddleware<{
name: string;
age: number;
email: string;
}>((path, nextValue) => {
if (!nextValue.name || nextValue.name.length < 2) {
return "Name must be at least 2 characters long";
}
if (nextValue.age < 18) {
return "User must be at least 18 years old";
}
if (!nextValue.email.includes("@")) {
return "Invalid email address";
}
return true;
});
// Async validation
const asyncValidator = createAsyncMiddleware<{
name: string;
age: number;
email: string;
}>(async (path, nextValue) => {
await new Promise((resolve) => setTimeout(resolve, 500));
console.log(`[Validation] Checking user data for ${nextValue.name}`);
return nextValue;
});
initTreeState({
initialState: {
user: {
name: "",
age: 0,
email: "",
},
},
middleware: [userValidator, asyncValidator],
});import {
initTreeState,
createDebounceMiddleware,
createPersistenceMiddleware,
createLoggingMiddleware,
} from "@bonsai-ts/state";
// Form state with debouncing, persistence, and logging
initTreeState({
initialState: {
form: {
username: "",
password: "",
email: "",
},
},
middleware: [
createDebounceMiddleware<{
username: string;
password: string;
email: string;
}>(300),
createLoggingMiddleware<{
username: string;
password: string;
email: string;
}>({
logPath: true,
logValue: false, // Don't log sensitive data
logPrevValue: false,
}),
createPersistenceMiddleware<{
username: string;
password: string;
email: string;
}>("formState"),
],
});For detailed documentation, check out our docs:
- Core Documentation - Learn about Bonsai's core concepts and architecture
- Usage Guide - Comprehensive guide with examples and best practices
-
Tree State
- Path-based state access (e.g., "user/profile/name")
- Nested state management
- Middleware support
- Type-safe updates
-
Flat State
- Simple key-value store
- Selector-based access
- Direct state updates
- Performance optimized
-
Scoped State
- Component-specific state
- Isolated state stores
- Type-safe access
- Automatic cleanup
-
Middleware
- Validation
- Logging
- Debouncing
- Persistence
- Custom middleware support
-
DevTools
- State visualization
- Real-time updates
- Path inspection
- Log viewer
- State modification
# Install dependencies
bun install
# Start development server
bun run dev
# Build library
bun run build
# Run tests
bun test
# Run bundle analysis
bun run analyze
# Run benchmarks
bun run benchmark
# Generate documentation
bun run docsContributions are welcome! Please read our Contributing Guide for details on our code of conduct and the process for submitting pull requests.
See CHANGELOG.md for release notes.
This project is licensed under the MIT License - see the LICENSE file for details.
- MIT Β© Prince Elorm(Akarikev)
