Skip to main content

API: store.set()

The store.set() method is the only way to modify the state of a SoulState store. It is a highly optimized function that ensures immutability, performs automatic batching, and initiates the notification process for subscribers.

Summary

store.set() updates the store's state by merging a partial state object or by applying a functional updater. It performs change detection to avoid unnecessary updates and triggers batched notifications to subscribers.

Function Signature

interface Store<T> {
set: (updater: StateUpdater<T>) => void;
// ... other methods
}

type StateUpdater<T> = Partial<T> | ((state: T) => Partial<T> | T);

Parameters

  • updater: This can be one of two types:
    1. Partial<T>: An object containing the properties you wish to update. These properties will be shallowly merged into the current state.
    2. (state: T) => Partial<T> | T: A function that receives the current state and returns either a partial update or complete new state. This is recommended for updates that depend on current state.

Return Value

store.set() does not return any value (void).

Usage Examples

1. Updating with a Partial State Object

Use this for updates that don't depend on the current state.

import { counterStore } from '../stores/counterStore';

// Set specific properties
counterStore.set({ count: 5 });

// Update multiple properties at once
counterStore.set({ count: 10, user: { name: 'John' } });

// Reset to initial state pattern
counterStore.set({ count: 0, user: null });

Use this for updates that depend on the current state, ensuring you work with the latest state.

import { counterStore } from '../stores/counterStore';

// Increment based on current value
counterStore.set((state) => ({ count: state.count + 1 }));

// Complex update with multiple properties
counterStore.set((state) => ({
count: state.count + 1,
user: state.user ? { ...state.user, lastActive: Date.now() } : null
}));

// Array operations
import { todoStore } from '../stores/todoStore';

todoStore.set((state) => ({
todos: state.todos.map(todo =>
todo.id === '123'
? { ...todo, completed: !todo.completed }
: todo
)
}));

Performance Optimizations

SoulState's set() method includes several performance optimizations:

Automatic Change Detection

// SoulState automatically detects if values actually changed
counterStore.set({ count: 5 }); // ✅ Update happens
counterStore.set({ count: 5 }); // 🚫 Skipped (same value)

// Even with objects, SoulState checks individual properties
const user = { name: 'John', age: 30 };
counterStore.set({ user }); // ✅ First update
counterStore.set({ user }); // 🚫 Skipped (same reference)
counterStore.set({ user: { ...user, age: 30 } }); // 🚫 Skipped (same values)

Microtask Batching

// Multiple updates in same event loop → single re-render
const handleMultipleUpdates = () => {
counterStore.set({ count: 1 }); // 🚫 No immediate notification
counterStore.set({ count: 2 }); // 🚫 No immediate notification
counterStore.set({ count: 3 }); // 🚫 No immediate notification
// ✅ Single notification with final state (count: 3)
};

// This applies to functional updates too
const handleComplexBatch = () => {
counterStore.set(state => ({ count: state.count + 1 }));
counterStore.set({ user: { name: 'Alice' } });
counterStore.set(state => ({ count: state.count * 2 }));
// ✅ One re-render with combined changes
};

Anti-patterns

1. Direct State Mutation

Never mutate the state object directly. Always return new objects.

// ❌ Anti-pattern: Direct mutation
counterStore.set((state) => {
state.count++; // Mutation! This breaks reactivity
return state; // Returns same object reference
});

// ❌ Anti-pattern: Nested mutation
counterStore.set((state) => {
state.user.age = 31; // Nested mutation!
return state;
});

// ✅ Correct: Return new objects
counterStore.set((state) => ({
count: state.count + 1 // New primitive
}));

// ✅ Correct: Nested updates with spread
counterStore.set((state) => ({
user: { ...state.user, age: 31 } // New object
}));

2. Using get() Instead of Functional Updates

Avoid unnecessary store interactions when functional updates suffice.

// ❌ Less optimal: Multiple store calls
const increment = () => {
const current = counterStore.get().count;
counterStore.set({ count: current + 1 });
};

// ✅ Better: Single atomic operation
const increment = () => {
counterStore.set(state => ({ count: state.count + 1 }));
};

3. Creating Unnecessary Objects

// ❌ Creates new object every time
const updateUser = (name: string) => {
counterStore.set({ user: { name, age: 30 } }); // New object always
};

// ✅ Only creates object when values change
const updateUser = (name: string) => {
counterStore.set(state => {
if (state.user?.name === name) return state; // No changes
return { user: { name, age: state.user?.age || 30 } };
});
};

Common Patterns

Toggle Pattern

// Boolean toggles
counterStore.set(state => ({ isActive: !state.isActive }));

// Multi-state toggles
counterStore.set(state => ({
status: state.status === 'idle' ? 'loading' : 'idle'
}));

Array Operations

// Add item
todoStore.set(state => ({
todos: [...state.todos, newTodo]
}));

// Remove item
todoStore.set(state => ({
todos: state.todos.filter(todo => todo.id !== idToRemove)
}));

// Update item
todoStore.set(state => ({
todos: state.todos.map(todo =>
todo.id === idToUpdate ? { ...todo, ...updates } : todo
)
}));

Conditional Updates

// Only update if condition met
counterStore.set(state => {
if (state.count >= 100) return state; // No update
return { count: state.count + 1 };
});

// Conditional property inclusion
counterStore.set(state => ({
...(state.user && { lastUser: state.user.name }),
count: state.count + 1
}));

Best Practices

✅ Do:

  • Use functional updates for state-dependent changes
  • Return new objects for nested updates
  • Leverage automatic batching for multiple updates
  • Trust SoulState's change detection optimizations
  • Use descriptive action names for complex updates

❌ Don't:

  • Mutate state objects directly
  • Use get() + set() when functional update suffices
  • Create unnecessary object references
  • Worry about micro-optimizations - SoulState handles them
⚠️

Immutability is Required

SoulState relies on immutable updates for change detection. Always return new objects from functional updaters and never mutate the current state.

ℹ️

Implementation Detail

The optimization logic can be found in src/core/store.ts where SoulState checks each updated property for actual value changes before creating new state objects.

Key Takeaway

store.set() is your gateway to state updates. With automatic batching, change detection, and microtask optimization, you get optimal performance by default. Focus on writing clear update logic, and let SoulState handle the performance optimizations.