State Management
Effuse provides global state management through @effuse/store.
Creating a Store
Use createStore from @effuse/store to create a reactive store:
import { createStore, connectDevTools } from '@effuse/store';
interface Todo {
id: number;
title: string;
completed: boolean;
}
interface TodosState {
todos: Todo[];
filter: 'all' | 'completed' | 'pending';
}
export const todosStore = createStore<
TodosState & {
addTodo: (todo: Todo) => void;
toggleTodo: (id: number) => void;
deleteTodo: (id: number) => void;
setFilter: (filter: 'all' | 'completed' | 'pending') => void;
}
>(
'todos', // Store name
{
// Initial state
todos: [],
filter: 'all',
// Actions - use 'this' to access state signals
addTodo(todo: Todo) {
this.todos.value = [todo, ...this.todos.value];
},
toggleTodo(id: number) {
this.todos.value = this.todos.value.map((t) =>
t.id === id ? { ...t, completed: !t.completed } : t
);
},
deleteTodo(id: number) {
this.todos.value = this.todos.value.filter((t) => t.id !== id);
},
setFilter(filter: 'all' | 'completed' | 'pending') {
this.filter.value = filter;
},
},
{ devtools: true } // Enable Redux DevTools
);
// Connect to Redux DevTools
connectDevTools(todosStore);
Advanced Selectors
Selectors allow you to derive state efficiently. Use createSelector for simple derivations and createSelectorAsync for asynchronous ones.
import { createSelector, combineSelectors } from '@effuse/store';
// Memoized selector
const selectCompletedTodos = createSelector(
(state) => state.todos,
(todos) => todos.filter((t) => t.completed)
);
// Combined selector
const selectStats = combineSelectors({
total: (state) => state.todos.length,
completed: selectCompletedTodos,
});
Slices and Composition
For large applications, you can split your store into slices and compose them.
import { defineSlice, composeStores } from '@effuse/store';
const userSlice = defineSlice('user', {
name: 'John Doe',
email: 'john@example.com',
});
const settingsSlice = defineSlice('settings', {
theme: 'dark',
});
export const rootStore = composeStores('root', {
user: userSlice,
settings: settingsSlice,
});
Cancellable and Async Actions
Effuse separates state mutation from async side-effects. For complex async logic with built-in concurrency control (like debouncing or throttling), use the useConcurrency hook provided by @effuse/store.
import { createStore, useConcurrency } from '@effuse/store';
export const searchStore = createStore('search', {
results: [],
setResults(data: any[]) {
this.results.value = data;
},
});
// Setup a concurrent action outside the store
export const performSearch = useConcurrency({
mode: 'switch', // Cancels previous pending requests (like takeLatest)
async action(query: string) {
const data = await api.search(query);
searchStore.setResults(data);
},
});
// Setup a debounced action
export const debouncedSearch = useConcurrency({
mode: 'switch',
debounce: 300,
async action(query: string) {
const data = await api.search(query);
searchStore.setResults(data);
},
});
Using Stores in Components
Import the store directly and use its state and actions:
import { define, computed, For } from '@effuse/core';
import { todosStore } from '../store/todosStore';
const TodoList = define({
script: () => {
// Destructure state and actions from store
const { todos, filter, toggleTodo, deleteTodo, setFilter } = todosStore;
// Create computed derived state
const filteredTodos = computed(() => {
switch (filter.value) {
case 'completed':
return todos.value.filter((t) => t.completed);
case 'pending':
return todos.value.filter((t) => !t.completed);
default:
return todos.value;
}
});
const totalCount = computed(() => todos.value.length);
return {
filteredTodos,
totalCount,
filter,
toggleTodo,
deleteTodo,
setFilter,
};
},
template: ({ filteredTodos, totalCount, filter, toggleTodo, setFilter }) => (
<div>
<p>Total: {totalCount}</p>
<div>
<button onClick={() => setFilter('all')}>All</button>
<button onClick={() => setFilter('completed')}>Completed</button>
<button onClick={() => setFilter('pending')}>Pending</button>
</div>
<For each={filteredTodos} keyExtractor={(t) => t.id}>
{(todoSignal) => (
<div onClick={() => toggleTodo(todoSignal.value.id)}>
{todoSignal.value.title}
</div>
)}
</For>
</div>
),
});
Store State is Reactive
Store state properties are automatically wrapped in signals:
// Access current value
const currentFilter = todosStore.filter.value;
// Update value (triggers reactivity)
todosStore.filter.value = 'completed';
// Or use action methods
todosStore.setFilter('completed');
Best Practices
Single Store per Domain: Create focused stores for each feature area
Actions for Mutations: Use action methods instead of direct mutation for clarity
DevTools Integration: Enable devtools for debugging state changes
Computed for Derived State: Create computed values in components, not stores
Next Steps