createStore

Create reactive stores with automatic TypeScript inference and built-in Immer support

API Reference

Syntax

const store = createStore(initialState);

Parameters

  • initialState: Object defining your store's initial state and actions

Returns

A reactive store instance with:

  • value: Current state (read/write access with dependency tracking)
  • setState(updater, options?): Update state with Immer support
  • subscribe(options): Fine-grained subscription control

Examples

Simple State Management

import { createStore, useStoreValue } from "zenbox";

// Types are automatically inferred
const counterStore = createStore({ count: 0 });

// Direct access state value via `value` property
console.log("Current count:", counterStore.value.count);

function Counter() {
  // Reactive subscription for components
  const { count } = useStoreValue(counterStore);

  const increment = () => {
    counterStore.setState((state) => {
      state.count++; // Immer-powered mutations
    });
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button>
    </div>
  );
}

Store with Actions

Co-locate state and behavior for clean, maintainable code:

const userStore = createStore({
  // State
  name: "Del Wang",
  blog: "https://del.wang",
  followers: 0,

  // Actions
  follow() {
    userStore.setState((state) => {
      state.followers++;
    });
  },

  async fetchProfile() {
    const response = await fetch("https://api.github.com/users/del-wang");
    const data = await response.json();
    userStore.setState(data);
  },
});

// Clean API calls
userStore.value.follow();
await userStore.value.fetchProfile();

Update Patterns

Perfect for complex mutations and nested state:

store.setState((state) => {
  state.count += 1;
  state.user.profile.name = "Updated";
  state.todos.push({ id: Date.now(), text: "New todo", completed: false });
});

2. Partial Object Updates

Merge new properties with existing state:

const store = createStore({
  name: "John",
  age: 25,
  email: "john@example.com",
  avatar: "avatar.jpg",
});

// Shallow merge with existing state
store.setState({ name: "Jane", age: 30 });
// Result: { name: "Jane", age: 30, email: "john@example.com", avatar: "avatar.jpg" }

// Explicit overwrites (including null/undefined)
store.setState({ avatar: null });
// Result: { name: "Jane", age: 30, email: "john@example.com", avatar: null }

// Unknown properties are ignored
store.setState({ unknownField: "ignored" }); // No effect
// Result: { name: "Jane", age: 30, email: "john@example.com", avatar: null }

3. Direct Assignment

For complete state replacement or when you prefer explicit assignment:

// Complete replacement
store.value = { count: 1, name: "Jane" };

// Partial updates (same as setState)
store.value = { count: 5 }; // Merges: { count: 5, name: "Jane" }

4. Silent Updates

Update state without triggering subscriptions / re-renders:

// Background cache updates
store.setState({ cache: freshData }, { silent: true });

Best Practices

Co-locate state and actions

const store = createStore({
  count: 0,
  increment() {
    /* action logic */
  },
});

Use Immer-style for complex updates

store.setState((state) => {
  state.nested.property = newValue;
});

Handle async operations properly

async fetchData() {
  store.setState(state => { state.loading = true; });
  try {
    // API call
  } finally {
    store.setState(state => { state.loading = false; });
  }
}

Use silent updates for background data

store.setState({ cache: data }, { silent: true });