API: React Hooks
SoulState provides powerful React hooks for declaratively binding your components to the store. These hooks are built on top of React.useSyncExternalStore for full compatibility with React 18's concurrent features.
useStore
This is the primary hook for consuming state in your components. It creates a subscription to a selected slice of your store's state and ensures your component re-renders only when that specific slice changes.
Signature
function useStore<T, S>(
store: Store<T>,
selector: (state: T) => S,
equalityFn?: (a: S, b: S) => boolean
): S;
Parameters
store: The store instance returned bycreateStore.selector: A function that receives the entire state and returns the desired value or object. The component will subscribe to changes in this returned value.equalityFn(optional): A function to compare the previous and current selected values. If the function returnstrue, the component will not re-render.- Default:
Object.is(strict reference equality).
- Default:
Return Value
The hook returns the value selected by your selector function.
Usage Examples
1. Selecting a single primitive value:
The component re-renders only when state.count changes.
import { useStore } from 'soulstate/react';
import { counterStore } from './store';
function CounterDisplay() {
const count = useStore(counterStore, (state) => state.count);
return <div>Count: {count}</div>;
}
2. Selecting multiple values with independent subscriptions: Each subscription is independent and optimized.
function UserProfile() {
const username = useStore(userStore, (state) => state.username);
const email = useStore(userStore, (state) => state.email);
const isActive = useStore(userStore, (state) => state.isActive);
return (
<div>
<p>Username: {username}</p>
<p>Email: {email}</p>
<p>Status: {isActive ? 'Active' : 'Inactive'}</p>
</div>
);
}
3. Using actions from separate exports: Actions are typically imported directly, not selected from store.
import { counterActions } from './store';
function Controls() {
return <button onClick={counterActions.increment}>Increment</button>;
}
Selector Best Practices
Always define selectors outside the component or memoize them with React.useCallback if they depend on props. This prevents the subscription from being torn down and recreated on every render.
Good (static selector):
const selectCount = (state) => state.count;
function Counter() {
const count = useStore(counterStore, selectCount);
// ...
}
Good (dynamic selector with props):
function TodoItem({ id }) {
const selectTodo = useCallback(
(state) => state.todos.find(todo => todo.id === id),
[id]
);
const todo = useStore(todoStore, selectTodo);
// ...
}
Avoid (inline selector):
function Counter() {
// ❌ Creates new function on every render
const count = useStore(counterStore, (state) => state.count);
// ...
}
useShallow
A convenience hook that combines useStore with shallow equality comparison. Perfect for selecting multiple properties into an object.
Signature
function useShallow<T, S>(
store: Store<T>,
selector: (state: T) => S
): S;
Usage
Use useShallow when your selector returns a new object containing multiple properties.
import { useShallow } from 'soulstate/react';
import { userStore } from './store';
function UserProfile() {
// Select multiple properties into a new object
const { username, email, preferences } = useShallow(
userStore,
(state) => ({
username: state.username,
email: state.email,
preferences: state.preferences
})
);
return (
<div>
<p>Username: {username}</p>
<p>Email: {email}</p>
</div>
);
}
shallow Equality Function
The standalone shallow equality function, also available for use with useStore.
Import Path
import { shallow } from 'soulstate/utils';
Usage with useStore
import { useStore } from 'soulstate/react';
import { shallow } from 'soulstate/utils';
function UserProfile() {
const userData = useStore(
userStore,
(state) => ({
username: state.username,
email: state.email
}),
shallow // Explicit shallow comparison
);
return <div>{userData.username}</div>;
}
Anti-Pattern without Proper Equality
Without shallow or useShallow, components that select multiple properties into objects will re-render on every state change. This happens because the selector creates a new object reference on every execution.
// ❌ Re-renders on every state change
const userData = useStore(
userStore,
(state) => ({ username: state.username, email: state.email })
// Missing equalityFn - uses Object.is which always fails for new objects
);
// ✅ Only re-renders when username or email actually change
const userData = useStore(
userStore,
(state) => ({ username: state.username, email: state.email }),
shallow
);
Performance Patterns
Multiple Independent Subscriptions vs Object Selection
// ✅ Pattern 1: Independent subscriptions (most granular)
function UserProfile() {
const username = useStore(userStore, state => state.username);
const email = useStore(userStore, state => state.email);
// Each updates independently when respective property changes
}
// ✅ Pattern 2: Object selection with shallow (convenient)
function UserProfile() {
const { username, email } = useShallow(userStore, state => ({
username: state.username,
email: state.email
}));
// Updates when either username OR email changes
}
// ❌ Pattern 3: Object selection without equality (inefficient)
function UserProfile() {
const { username, email } = useStore(userStore, state => ({
username: state.username,
email: state.email
}));
// Updates on EVERY state change
}
Derived State with Selectors
function TodoStats() {
// Derived state computed in selectors
const totalTodos = useStore(todoStore, state => state.todos.length);
const completedTodos = useStore(todoStore,
state => state.todos.filter(todo => todo.completed).length
);
const activeTodos = totalTodos - completedTodos;
return (
<div>
<p>Total: {totalTodos}</p>
<p>Completed: {completedTodos}</p>
<p>Active: {activeTodos}</p>
</div>
);
}
Integration with React 18+
SoulState hooks are built on React.useSyncExternalStore, making them fully compatible with:
- Concurrent Features: Safe usage in concurrent rendering
- Strict Mode: Proper double-invocation handling
- Suspense: Compatible with React Suspense boundaries
- Server Components: Can be used in client components
Implementation Detail
The React hooks implementation can be found in src/react/useStore.ts, which leverages useSyncExternalStore for optimal React 18+ integration.
Key Takeaway
SoulState's React hooks provide surgical precision for component updates. By combining precise selectors with efficient equality checks, you get optimal performance with minimal boilerplate.