eleva

Store API Reference

Store Plugin Complete API reference.

Properties

Property Type Description
store.state Object Reactive state object with Signal properties

Store Access

The Store plugin provides two access points with slightly different APIs:

ctx.store (in component setup): Available methods: state, dispatch, subscribe, getState, registerModule, unregisterModule, createState, createAction, signal

app.store (full Store instance): All of the above, plus: replaceState, clearPersistedState

// In component setup - use ctx.store
app.component("MyComponent", {
  setup({ store }) {
    // store.dispatch, store.state, etc. are available
    // store.replaceState is NOT available here
  }
});

// Outside components - use app.store
app.store.replaceState(newState);  // Only available on app.store
app.store.clearPersistedState();   // Only available on app.store

Methods

dispatch(actionName, payload?)

Executes an action to mutate state.

Note: Always returns a Promise regardless of whether the action is sync or async. Subscriber callbacks that throw are caught and passed to the onError handler.

Emits the following events via eleva.emitter:

// Signature
store.dispatch(actionName: string, payload?: any): Promise<any>

// Examples
await store.dispatch("increment");
await store.dispatch("setUser", { name: "John", email: "john@example.com" });
await store.dispatch("auth.login", { username: "john", password: "secret" });
const user = await store.dispatch("fetchUser", 123);

subscribe(callback)

Subscribes to all state mutations. Returns an unsubscribe function.

// Signature
store.subscribe(callback: (mutation, state) => void): () => void

// Example
const unsubscribe = store.subscribe((mutation, state) => {
  console.log("Action:", mutation.type);
  console.log("Payload:", mutation.payload);
  console.log("Timestamp:", mutation.timestamp);
  console.log("New state:", state);
});

// Later: stop listening
unsubscribe();

getState()

Returns current state values (non-reactive snapshot).

Note: When persistence include/exclude filters are configured, this returns only the filtered subset of state, not the full state.

// Signature
store.getState(): Object

// Example
const snapshot = store.getState();
console.log(snapshot);
// { count: 5, user: { name: "John" }, auth: { token: "abc" } }

// Useful for debugging, logging, or SSR hydration
localStorage.setItem("debug-state", JSON.stringify(store.getState()));

replaceState(newState)

Replaces state values. Useful for hydration or time-travel debugging.

Note: When persistence include/exclude filters are configured, this only updates the filtered subset of state, not all properties.

// Signature
store.replaceState(newState: Object): void

// Example
store.replaceState({
  count: 10,
  user: { name: "Jane", email: "jane@example.com" },
  theme: "dark"
});

registerModule(namespace, module)

Dynamically registers a new namespaced module. Emits store:register via eleva.emitter with { namespace, timestamp }.

// Signature
store.registerModule(namespace: string, module: { state: Object, actions: Object }): void

// Example
store.registerModule("wishlist", {
  state: {
    items: []
  },
  actions: {
    addItem: (state, item) => {
      state.wishlist.items.value = [...state.wishlist.items.value, item];
    },
    removeItem: (state, itemId) => {
      state.wishlist.items.value = state.wishlist.items.value.filter(i => i.id !== itemId);
    }
  }
});

// Now accessible
store.state.wishlist.items.value;
store.dispatch("wishlist.addItem", { id: 1, name: "Product" });

unregisterModule(namespace)

Removes a dynamically registered module. Emits store:unregister via eleva.emitter with { namespace, timestamp }.

// Signature
store.unregisterModule(namespace: string): void

// Example
store.unregisterModule("wishlist");
// store.state.wishlist is now undefined

createState(key, initialValue)

Creates a new state property at runtime.

// Signature
store.createState(key: string, initialValue: any): Signal

// Example
const theme = store.createState("theme", "dark");
console.log(theme.value); // "dark"
console.log(store.state.theme.value); // "dark"

createAction(name, actionFn)

Creates a new action at runtime. Supports dot-notation for namespaced actions.

// Signature
store.createAction(name: string, actionFn: (state, payload?) => any | Promise<any>): void

// Example - root-level action
store.createAction("toggleTheme", (state) => {
  state.theme.value = state.theme.value === "dark" ? "light" : "dark";
});
store.dispatch("toggleTheme");

// Namespaced action using dot-notation
store.createAction("auth.logout", (state) => {
  state.auth.user.value = null;
  state.auth.token.value = null;
});
store.dispatch("auth.logout");

signal

Reference to the Signal class, available for creating local reactive state within components.

// Signature
store.signal: typeof Signal

// Example - Create local reactive state using store's signal reference
setup({ store }) {
  // Use store.signal to create local reactive state (requires 'new')
  const localCount = new store.signal(0);

  return {
    localCount,
    increment: () => localCount.value++
  };
}

Important: store.signal is the Signal class and requires the new keyword. This differs from ctx.signal which is a factory function that doesn’t require new. Use ctx.signal(0) for convenience, or new store.signal(0) when you only have access to the store object.

clearPersistedState()

Clears persisted state from storage.

// Signature
store.clearPersistedState(): void

// Example
store.clearPersistedState();
// localStorage/sessionStorage entry is removed

Emitter Events

The Store plugin emits events via eleva.emitter for cross-plugin observability. These events can be captured by the Agent plugin’s emitterEvents option or any custom listener.

Event Fired When Payload
store:dispatch Before action execution { type, payload, timestamp }
store:mutate After successful action execution { type, payload, timestamp }
store:error When an action throws { action, error, timestamp }
store:register Namespace module registered { namespace, timestamp }
store:unregister Namespace module unregistered { namespace, timestamp }
// Listen for Store events directly
app.emitter.on("store:mutate", (mutation) => {
  console.log(`State changed: ${mutation.type}`);
});

// Or capture in Agent audit log
app.use(Agent, {
  emitterEvents: ["store:"]
});

Eleva Instance Shortcuts

The plugin also adds shortcuts to the Eleva instance:

// These are equivalent:
app.store.dispatch("increment");
app.dispatch("increment");

app.store.getState();
app.getState();

app.store.subscribe(callback);
app.subscribe(callback);

app.store.createAction("foo", fn);
app.createAction("foo", fn);

Uninstalling the Plugin

The Store plugin provides an uninstall() method to completely remove it from an Eleva instance.

Store.uninstall(app)

Removes the Store plugin and restores the original Eleva behavior.

import Eleva from "eleva";
import { Store } from "eleva/plugins";

const app = new Eleva("MyApp");
app.use(Store, {
  state: { count: 0 },
  actions: { increment: (state) => state.count.value++ }
});

// Use the store...
app.store.dispatch("increment");

// Later, to completely remove the Store plugin:
Store.uninstall(app);

// After uninstall, these are removed:
// - app.store (undefined)
// - app.dispatch (undefined)
// - app.getState (undefined)
// - app.subscribe (undefined)
// - app.createAction (undefined)
// - Original mount() method is restored

What Store.uninstall() Does

  1. Restores original methods:
    • app.mount → restored to original
    • app._mountComponents → restored to original
  2. Removes added properties:
    • app.store
    • app.dispatch
    • app.getState
    • app.subscribe
    • app.createAction

When to Use

Uninstall Order (LIFO)

When using multiple plugins, uninstall in reverse order of installation:

// Installation order
app.use(Attr);
app.use(Store, { state: {} });
app.use(Router, { routes: [] });

// Uninstall in reverse order (LIFO)
await Router.uninstall(app);  // Last installed, first uninstalled
Store.uninstall(app);
Attr.uninstall(app);

Note: Store’s uninstall() is synchronous (not async), unlike Router’s which is async.


TypeScript Support

The Store plugin includes TypeScript type definitions for full type safety.

Basic Typed Store

import Eleva from "eleva";
import { Store } from "eleva/plugins";

// Define your state shape
interface AppState {
  count: number;
  user: User | null;
  theme: "light" | "dark";
}

interface User {
  id: number;
  name: string;
  email: string;
}

// Define action payloads
interface ActionPayloads {
  increment: void;
  setCount: number;
  setUser: User | null;
  setTheme: "light" | "dark";
}

const app = new Eleva("TypedApp");

app.use(Store, {
  state: {
    count: 0,
    user: null,
    theme: "light"
  } as AppState,

  actions: {
    increment: (state) => {
      state.count.value++;
    },
    setCount: (state, value: number) => {
      state.count.value = value;
    },
    setUser: (state, user: User | null) => {
      state.user.value = user;
    },
    setTheme: (state, theme: "light" | "dark") => {
      state.theme.value = theme;
    }
  }
});

// Type-safe dispatch
app.store.dispatch("setCount", 10);
app.store.dispatch("setUser", { id: 1, name: "John", email: "john@example.com" });

Typed Component Setup

interface ComponentContext {
  store: {
    state: {
      count: Signal<number>;
      user: Signal<User | null>;
    };
    dispatch: (action: string, payload?: unknown) => Promise<unknown>;
  };
  signal: <T>(value: T) => Signal<T>;
}

app.component("TypedComponent", {
  setup({ store, signal }: ComponentContext) {
    const localValue = signal<string>("");

    const increment = () => store.dispatch("increment");
    const setUser = (user: User) => store.dispatch("setUser", user);

    return {
      count: store.state.count,
      user: store.state.user,
      localValue,
      increment,
      setUser
    };
  },
  template: (ctx) => `
    <div>
      <p>Count: ${ctx.count.value}</p>
      <p>User: ${ctx.user.value?.name || "Guest"}</p>
    </div>
  `
});

Typed Namespaces

interface AuthState {
  user: User | null;
  token: string | null;
  isAuthenticated: boolean;
}

interface CartState {
  items: CartItem[];
  total: number;
}

interface CartItem {
  id: number;
  name: string;
  price: number;
  quantity: number;
}

app.use(Store, {
  namespaces: {
    auth: {
      state: {
        user: null,
        token: null,
        isAuthenticated: false
      } as AuthState,
      actions: {
        login: (state, payload: { user: User; token: string }) => {
          state.auth.user.value = payload.user;
          state.auth.token.value = payload.token;
          state.auth.isAuthenticated.value = true;
        },
        logout: (state) => {
          state.auth.user.value = null;
          state.auth.token.value = null;
          state.auth.isAuthenticated.value = false;
        }
      }
    },
    cart: {
      state: {
        items: [],
        total: 0
      } as CartState,
      actions: {
        addItem: (state, item: CartItem) => {
          state.cart.items.value = [...state.cart.items.value, item];
          state.cart.total.value += item.price * item.quantity;
        }
      }
    }
  }
});

Troubleshooting

State Not Updating

Problem: Component doesn’t re-render when state changes.

Solutions:

// 1. Make sure you're using .value
// Wrong
template: (ctx) => `<p>${ctx.count}</p>`
// Right
template: (ctx) => `<p>${ctx.count.value}</p>`

// 2. Make sure you're returning state from setup
setup({ store }) {
  return {
    count: store.state.count  // Must return the signal
  };
}

// 3. For arrays/objects, create new references
// Wrong - mutating existing array
state.todos.value.push(newTodo);
// Right - creating new array
state.todos.value = [...state.todos.value, newTodo];

Action Not Found

Problem: Error: Action "actionName" not found

Solutions:

// 1. Check action name spelling
store.dispatch("increment");  // Must match exactly

// 2. For namespaced actions, use dot notation
store.dispatch("auth.login", payload);  // Not "authLogin"

// 3. Make sure action is defined
app.use(Store, {
  actions: {
    increment: (state) => state.count.value++  // Must exist
  }
});

Persistence Not Working

Problem: State not being saved/loaded from storage.

Solutions:

// 1. Check persistence is enabled
persistence: {
  enabled: true,  // Must be true
  key: "my-store"
}

// 2. Check include/exclude paths
persistence: {
  include: ["theme"]  // Only these are persisted
}

// 3. Check storage availability
if (typeof window !== "undefined" && window.localStorage) {
  // Storage is available
}

// 4. Check for storage quota errors
onError: (error, context) => {
  if (error.name === "QuotaExceededError") {
    console.warn("Storage quota exceeded");
  }
}

Module Already Exists

Problem: Module "name" already exists warning.

Solution:

// Check before registering
if (!store.state.myModule) {
  store.registerModule("myModule", { /* ... */ });
}

// Or unregister first
store.unregisterModule("myModule");
store.registerModule("myModule", { /* ... */ });

Circular State Updates

Problem: Infinite loop of state updates.

Solution:

// Avoid updating state in watchers that trigger re-renders
// Wrong
store.state.count.watch(() => {
  store.state.count.value++;  // Infinite loop!
});

// Right - use conditions
store.state.count.watch((value) => {
  if (value < 100) {
    // Safe conditional update
  }
});

Migration Guide

From Vuex

Vuex Eleva Store
state: { count: 0 } state: { count: 0 }
mutations actions (combined)
actions actions (async supported)
getters Computed in components
modules namespaces
this.$store.state.count store.state.count.value
this.$store.commit('increment') store.dispatch('increment')
this.$store.dispatch('fetchData') store.dispatch('fetchData')
mapState, mapGetters Destructure in setup()

From Redux

Redux Eleva Store
createStore(reducer) app.use(Store, { state, actions })
Reducers (pure functions) actions (mutate directly)
store.getState() store.getState() or store.state.*.value
store.dispatch({ type, payload }) store.dispatch("type", payload)
store.subscribe() store.subscribe()
combineReducers namespaces
connect() HOC Access in setup({ store })
Middleware subscribe() + custom logic
Redux Thunk Actions support async natively

From MobX

MobX Eleva Store
observable({ count: 0 }) state: { count: 0 } (auto-wrapped in Signals)
action decorator Define in actions object
computed Compute in components
observer HOC Not needed (automatic)
autorun store.subscribe()
runInAction Just call dispatch()

Summary

Store Statistics

Metric Value
Bundle Size ~6KB minified (~2KB gzipped)
API Methods 11 (dispatch, subscribe, getState, signal, etc.)
Storage Options 2 (localStorage, sessionStorage)
State Access Direct via Signals
Async Support Native (actions can be async)

Core API Quick Reference

Method Purpose
store.state Access reactive state
store.dispatch(action, payload) Execute action
store.subscribe(callback) Listen to mutations
store.getState() Get state snapshot (respects persistence filters)
store.replaceState(state) Replace state (respects persistence filters)
store.registerModule(name, module) Add module
store.unregisterModule(name) Remove module
store.createState(key, value) Add state property
store.createAction(name, fn) Add action
store.signal Signal class (use with new)
store.clearPersistedState() Clear storage

Key Concepts

  1. State - Reactive data wrapped in Signals
  2. Actions - Functions that mutate state
  3. Namespaces - Modular organization
  4. Persistence - Automatic storage sync
  5. Subscriptions - React to all mutations
  6. Dynamic Modules - Runtime extensibility

For questions or issues, visit the GitHub repository.


See Also


← Back to Advanced Back to Store Overview Examples →