This guide covers the fundamental concepts of the Store plugin: state, actions, and namespaces.
State is the single source of truth for your application. All state properties are automatically wrapped in Eleva Signals, making them reactive.
// file: store-setup.js
app.use(Store, {
state: {
// Primitives
count: 0,
name: "Guest",
isLoggedIn: false,
// Objects
user: {
id: null,
name: "",
email: ""
},
// Arrays
todos: [],
notifications: []
}
});
// file: my-component.js
app.component("MyComponent", {
setup({ store }) {
// Access state signals directly
const count = store.state.count; // Signal<number>
const user = store.state.user; // Signal<object>
const todos = store.state.todos; // Signal<array>
return { count, user, todos };
},
template: (ctx) => `
<div>
<p>Count: ${ctx.count.value}</p>
<p>User: ${ctx.user.value?.name || 'Guest'}</p>
<p>Todos: ${ctx.todos.value.length}</p>
</div>
`
});
// Result: Displays count, user name, and todo count
// UI automatically updates when state changes
Important: State properties are Signals. Always use .value to read or write:
// Reading
const currentCount = store.state.count.value;
// Writing (prefer actions for mutations)
store.state.count.value = 10;
Best Practice: Use actions for all state mutations to enable tracking and debugging.
Actions are functions that mutate state. They provide a predictable way to change state and enable tracking/debugging.
// file: store-actions.js
app.use(Store, {
state: {
count: 0,
todos: []
},
actions: {
// Simple action - no payload
increment: (state) => {
state.count.value++;
},
// Action with payload
incrementBy: (state, amount) => {
state.count.value += amount;
},
// Action with object payload
addTodo: (state, { title, priority }) => {
state.todos.value = [
...state.todos.value,
{ id: Date.now(), title, priority, done: false }
];
},
// Async action
fetchUser: async (state, userId) => {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
state.user.value = user;
return user; // Actions can return values
}
}
});
// file: component-with-actions.js
app.component("TodoManager", {
setup({ store }) {
// Simple dispatch
const increment = () => store.dispatch("increment");
// Dispatch with payload
const incrementBy = (n) => store.dispatch("incrementBy", n);
// Dispatch with object payload
const addTodo = (title) => store.dispatch("addTodo", {
title,
priority: "normal"
});
// Async dispatch
const loadUser = async (id) => {
const user = await store.dispatch("fetchUser", id);
console.log("Loaded:", user);
};
return { increment, incrementBy, addTodo, loadUser };
}
});
Critical: Store state uses signals internally, which detect changes via identity comparison (
===). Array methods like.push(),.pop(),.splice()mutate the existing array without changing its reference, so the UI won’t update.
Always create new references for proper reactivity:
// Good - new array reference triggers update
state.todos.value = [...state.todos.value, newTodo];
state.todos.value = state.todos.value.filter(t => t.id !== id);
state.todos.value = state.todos.value.map(t => t.id === id ? {...t, done: true} : t);
// Bad - mutation doesn't trigger update (same reference!)
state.todos.value.push(newTodo); // Won't re-render!
state.todos.value.splice(index, 1); // Won't re-render!
state.todos.value[0].done = true; // Won't re-render!
For larger applications, organize state into namespaced modules:
// file: store-with-namespaces.js
app.use(Store, {
// Root state
state: {
appName: "MyApp",
theme: "light"
},
// Root actions
actions: {
setTheme: (state, theme) => state.theme.value = theme
},
// Namespaced modules
namespaces: {
// Auth module
auth: {
state: {
user: null,
token: null,
isAuthenticated: false
},
actions: {
login: (state, { user, token }) => {
state.auth.user.value = user;
state.auth.token.value = token;
state.auth.isAuthenticated.value = true;
},
logout: (state) => {
state.auth.user.value = null;
state.auth.token.value = null;
state.auth.isAuthenticated.value = false;
}
}
},
// Cart module
cart: {
state: {
items: [],
total: 0
},
actions: {
addItem: (state, item) => {
state.cart.items.value = [...state.cart.items.value, item];
state.cart.total.value += item.price;
},
removeItem: (state, itemId) => {
const item = state.cart.items.value.find(i => i.id === itemId);
if (item) {
state.cart.items.value = state.cart.items.value.filter(i => i.id !== itemId);
state.cart.total.value -= item.price;
}
},
clearCart: (state) => {
state.cart.items.value = [];
state.cart.total.value = 0;
}
}
}
}
});
// file: namespaced-component.js
app.component("Header", {
setup({ store }) {
// Access namespaced state
const user = store.state.auth.user;
const isAuthenticated = store.state.auth.isAuthenticated;
const cartItems = store.state.cart.items;
const cartTotal = store.state.cart.total;
// Dispatch namespaced actions (use dot notation)
const login = (credentials) => store.dispatch("auth.login", credentials);
const logout = () => store.dispatch("auth.logout");
const addToCart = (item) => store.dispatch("cart.addItem", item);
return { user, isAuthenticated, cartItems, cartTotal, login, logout, addToCart };
},
template: (ctx) => `
<header>
${ctx.isAuthenticated.value
? `<span>Welcome, ${ctx.user.value.name}</span>
<span>Cart: ${ctx.cartItems.value.length} items ($${ctx.cartTotal.value})</span>
<button @click="logout">Logout</button>`
: `<button @click="() => login({ user: { name: 'John' }, token: 'abc' })">Login</button>`
}
</header>
`
});
// Result: Shows login button or user info + cart based on auth state
// Good - flat structure
state: {
userId: null,
userName: "",
userEmail: "",
todos: []
}
// Avoid - deeply nested
state: {
user: {
profile: {
details: {
name: "",
email: ""
}
}
}
}
// Good - use actions
store.dispatch("setUser", newUser);
// Avoid - direct mutation (harder to track/debug)
store.state.user.value = newUser;
// Good - pure action
actions: {
addTodo: (state, todo) => {
state.todos.value = [...state.todos.value, todo];
}
}
// For async operations, create wrapper actions
store.createAction("fetchAndAddTodo", async (state, todoId) => {
const todo = await api.getTodo(todoId);
await store.dispatch("addTodo", todo);
});
// Good - organized by feature
namespaces: {
auth: { state: {...}, actions: {...} },
cart: { state: {...}, actions: {...} },
products: { state: {...}, actions: {...} }
}
setup({ store }) {
// Good - compute in component
const completedTodos = () =>
store.state.todos.value.filter(t => t.completed);
const totalPrice = () =>
store.state.cart.items.value.reduce((sum, i) => sum + i.price, 0);
return { completedTodos, totalPrice };
}
| ← Back to Store | Next: Configuration → |