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

  1. Single Store per Domain: Create focused stores for each feature area

  2. Actions for Mutations: Use action methods instead of direct mutation for clarity

  3. DevTools Integration: Enable devtools for debugging state changes

  4. Computed for Derived State: Create computed values in components, not stores

Next Steps