Introduction
While state management libraries like Redux, MobX, and Zustand are popular, many applications can benefit from lightweight, custom state management solutions. Building your own state management system gives you complete control, eliminates dependencies, and often results in simpler, more maintainable code.
This guide explores practical approaches to managing application state using only vanilla JavaScript, from simple reactive patterns to complex nested state architectures.
Simple Reactive State
Basic Observable State
Let's start with a simple reactive state implementation:
// Simple reactive state manager
class ReactiveState {
constructor(initialState = {}) {
this._state = { ...initialState };
this._subscribers = new Set();
}
// Get current state (immutable)
getState() {
return { ...this._state };
}
// Subscribe to state changes
subscribe(callback) {
this._subscribers.add(callback);
// Return unsubscribe function
return () => {
this._subscribers.delete(callback);
};
}
// Update state and notify subscribers
setState(updates) {
const prevState = this.getState();
this._state = { ...this._state, ...updates };
// Notify all subscribers
this._subscribers.forEach(callback => {
try {
callback(this.getState(), prevState);
} catch (error) {
console.error('Error in state subscriber:', error);
}
});
}
// Reset to initial state
reset(initialState = {}) {
this._state = { ...initialState };
this._subscribers.forEach(callback => {
callback(this.getState(), {});
});
}
}
// Usage example
const appState = new ReactiveState({
user: null,
theme: 'light',
notifications: []
});
// Subscribe to state changes
const unsubscribe = appState.subscribe((newState, prevState) => {
console.log('State changed:', {
from: prevState,
to: newState
});
// Update UI based on state changes
updateUI(newState);
});
// Update state
appState.setState({
user: { name: 'John Doe', email: 'john@example.com' },
theme: 'dark'
});
Computed Properties and Derived State
// Enhanced state manager with computed properties
class ComputedState extends ReactiveState {
constructor(initialState = {}, computedProperties = {}) {
super(initialState);
this._computed = computedProperties;
this._computedCache = new Map();
}
getState() {
const baseState = super.getState();
const computedState = {};
// Calculate computed properties
Object.entries(this._computed).forEach(([key, computeFn]) => {
try {
// Simple memoization
const cacheKey = JSON.stringify(baseState);
if (this._computedCache.has(`${key}:${cacheKey}`)) {
computedState[key] = this._computedCache.get(`${key}:${cacheKey}`);
} else {
const value = computeFn(baseState);
this._computedCache.set(`${key}:${cacheKey}`, value);
computedState[key] = value;
}
} catch (error) {
console.error(`Error computing ${key}:`, error);
computedState[key] = null;
}
});
return { ...baseState, ...computedState };
}
setState(updates) {
// Clear computed cache when state changes
this._computedCache.clear();
super.setState(updates);
}
}
// Example with computed properties
const shoppingCartState = new ComputedState(
{
items: [],
tax: 0.08,
discountCode: null
},
{
// Computed: total items count
itemCount: (state) => state.items.length,
// Computed: subtotal
subtotal: (state) => state.items.reduce((sum, item) => {
return sum + (item.price * item.quantity);
}, 0),
// Computed: tax amount
taxAmount: (state) => {
const subtotal = state.items.reduce((sum, item) => {
return sum + (item.price * item.quantity);
}, 0);
return subtotal * state.tax;
},
// Computed: total with tax
total: (state) => {
const subtotal = state.items.reduce((sum, item) => {
return sum + (item.price * item.quantity);
}, 0);
const tax = subtotal * state.tax;
return subtotal + tax;
},
// Computed: is cart empty
isEmpty: (state) => state.items.length === 0
}
);
// Usage
shoppingCartState.setState({
items: [
{ id: 1, name: 'Laptop', price: 999, quantity: 1 },
{ id: 2, name: 'Mouse', price: 29, quantity: 2 }
]
});
const state = shoppingCartState.getState();
console.log({
itemCount: state.itemCount, // 2
subtotal: state.subtotal, // 1057
taxAmount: state.taxAmount, // 84.56
total: state.total, // 1141.56
isEmpty: state.isEmpty // false
});
Action-Based State Management
Redux-Like Pattern Without Redux
// Action-based state manager
class ActionState {
constructor(initialState = {}, reducers = {}) {
this._state = { ...initialState };
this._reducers = reducers;
this._subscribers = new Set();
this._middlewares = [];
}
// Add middleware for side effects
use(middleware) {
this._middlewares.push(middleware);
}
// Subscribe to state changes
subscribe(callback) {
this._subscribers.add(callback);
return () => this._subscribers.delete(callback);
}
// Get current state
getState() {
return { ...this._state };
}
// Dispatch action
async dispatch(action) {
// Run middlewares
let processedAction = action;
for (const middleware of this._middlewares) {
processedAction = await middleware(processedAction, this.getState(), this);
if (!processedAction) break; // Middleware can cancel action
}
if (!processedAction) return;
// Find and execute reducer
const { type, payload } = processedAction;
const reducer = this._reducers[type];
if (!reducer) {
console.warn(`No reducer found for action type: ${type}`);
return;
}
try {
const prevState = this.getState();
const newState = reducer(prevState, payload);
// Ensure immutability
this._state = { ...newState };
// Notify subscribers
this._subscribers.forEach(callback => {
callback(this.getState(), prevState, action);
});
return processedAction;
} catch (error) {
console.error(`Error in reducer for ${type}:`, error);
}
}
}
// Todo app example
const todoReducers = {
ADD_TODO: (state, todo) => ({
...state,
todos: [
...state.todos,
{
id: Date.now(),
text: todo.text,
completed: false,
createdAt: new Date().toISOString()
}
]
}),
TOGGLE_TODO: (state, { id }) => ({
...state,
todos: state.todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
)
}),
DELETE_TODO: (state, { id }) => ({
...state,
todos: state.todos.filter(todo => todo.id !== id)
}),
SET_FILTER: (state, { filter }) => ({
...state,
filter
}),
SET_LOADING: (state, { loading }) => ({
...state,
loading
})
};
// Create store
const todoStore = new ActionState(
{
todos: [],
filter: 'all', // all, active, completed
loading: false
},
todoReducers
);
// Middleware for logging
const loggerMiddleware = (action, state, store) => {
console.group(`Action: ${action.type}`);
console.log('Payload:', action.payload);
console.log('Current State:', state);
console.groupEnd();
return action; // Continue with action
};
// Middleware for persistence
const persistenceMiddleware = (action, state, store) => {
// Skip persistence for certain actions
if (action.type === 'SET_LOADING') {
return action;
}
// Save to localStorage after action completes
setTimeout(() => {
localStorage.setItem('todoState', JSON.stringify(store.getState()));
}, 0);
return action;
};
// Add middlewares
todoStore.use(loggerMiddleware);
todoStore.use(persistenceMiddleware);
// Subscribe to changes
todoStore.subscribe((newState, prevState, action) => {
renderTodoApp(newState);
});
// Dispatch actions
todoStore.dispatch({
type: 'ADD_TODO',
payload: { text: 'Learn vanilla state management' }
});
todoStore.dispatch({
type: 'ADD_TODO',
payload: { text: 'Build awesome apps' }
});
todoStore.dispatch({
type: 'TOGGLE_TODO',
payload: { id: todoStore.getState().todos[0].id }
});
Async State Management
Handling Async Operations
// Async state manager with request handling
class AsyncState extends ActionState {
constructor(initialState = {}, reducers = {}) {
super(initialState, reducers);
this._pendingRequests = new Map();
}
// Async action creator
createAsyncAction(type, asyncFn) {
return async (payload) => {
const requestId = Date.now() + Math.random();
// Dispatch loading state
await this.dispatch({
type: `${type}_LOADING`,
payload: { requestId, loading: true }
});
try {
// Store pending request for cancellation
const abortController = new AbortController();
this._pendingRequests.set(requestId, abortController);
// Execute async function
const result = await asyncFn(payload, {
signal: abortController.signal,
dispatch: this.dispatch.bind(this),
getState: this.getState.bind(this)
});
// Dispatch success
await this.dispatch({
type: `${type}_SUCCESS`,
payload: { requestId, data: result }
});
return result;
} catch (error) {
if (error.name === 'AbortError') {
await this.dispatch({
type: `${type}_CANCELLED`,
payload: { requestId }
});
} else {
await this.dispatch({
type: `${type}_ERROR`,
payload: { requestId, error: error.message }
});
}
throw error;
} finally {
this._pendingRequests.delete(requestId);
}
};
}
// Cancel specific request
cancelRequest(requestId) {
const controller = this._pendingRequests.get(requestId);
if (controller) {
controller.abort();
this._pendingRequests.delete(requestId);
}
}
// Cancel all pending requests
cancelAllRequests() {
this._pendingRequests.forEach(controller => controller.abort());
this._pendingRequests.clear();
}
}
// API state management example
const apiReducers = {
FETCH_USERS_LOADING: (state, { requestId, loading }) => ({
...state,
users: {
...state.users,
loading,
error: null
}
}),
FETCH_USERS_SUCCESS: (state, { data }) => ({
...state,
users: {
data,
loading: false,
error: null,
lastFetched: new Date().toISOString()
}
}),
FETCH_USERS_ERROR: (state, { error }) => ({
...state,
users: {
...state.users,
loading: false,
error
}
}),
FETCH_USER_PROFILE_SUCCESS: (state, { data }) => ({
...state,
currentUser: data,
userProfile: {
...state.userProfile,
loading: false,
error: null
}
})
};
const apiStore = new AsyncState(
{
users: {
data: [],
loading: false,
error: null,
lastFetched: null
},
currentUser: null,
userProfile: {
loading: false,
error: null
}
},
apiReducers
);
// Create async actions
const fetchUsers = apiStore.createAsyncAction('FETCH_USERS', async (params, { signal }) => {
const response = await fetch('/api/users?' + new URLSearchParams(params), { signal });
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
});
const fetchUserProfile = apiStore.createAsyncAction('FETCH_USER_PROFILE', async (userId, { signal, dispatch, getState }) => {
// Check cache first
const currentState = getState();
if (currentState.currentUser?.id === userId) {
return currentState.currentUser;
}
const response = await fetch(`/api/users/${userId}`, { signal });
return response.json();
});
// Usage
async function loadUserData() {
try {
await fetchUsers({ page: 1, limit: 10 });
const users = apiStore.getState().users.data;
if (users.length > 0) {
await fetchUserProfile(users[0].id);
}
} catch (error) {
console.error('Failed to load user data:', error);
}
}
Nested State Management
Complex State Structures
// Deep state manager with path-based updates
class DeepState {
constructor(initialState = {}) {
this._state = this._deepClone(initialState);
this._subscribers = new Set();
this._pathSubscribers = new Map();
}
_deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof Array) return obj.map(item => this._deepClone(item));
const cloned = {};
Object.keys(obj).forEach(key => {
cloned[key] = this._deepClone(obj[key]);
});
return cloned;
}
_getByPath(obj, path) {
return path.split('.').reduce((current, key) => {
return current && current[key] !== undefined ? current[key] : undefined;
}, obj);
}
_setByPath(obj, path, value) {
const keys = path.split('.');
const lastKey = keys.pop();
const target = keys.reduce((current, key) => {
if (!current[key] || typeof current[key] !== 'object') {
current[key] = {};
}
return current[key];
}, obj);
target[lastKey] = value;
}
// Subscribe to specific path changes
subscribeToPath(path, callback) {
if (!this._pathSubscribers.has(path)) {
this._pathSubscribers.set(path, new Set());
}
this._pathSubscribers.get(path).add(callback);
return () => {
const subscribers = this._pathSubscribers.get(path);
if (subscribers) {
subscribers.delete(callback);
if (subscribers.size === 0) {
this._pathSubscribers.delete(path);
}
}
};
}
// Get value at path
get(path = '') {
if (!path) return this._deepClone(this._state);
return this._deepClone(this._getByPath(this._state, path));
}
// Set value at path
set(path, value) {
const prevState = this._deepClone(this._state);
const prevValue = this._getByPath(this._state, path);
this._setByPath(this._state, path, value);
// Notify path subscribers
const pathSubscribers = this._pathSubscribers.get(path);
if (pathSubscribers) {
pathSubscribers.forEach(callback => {
callback(this._deepClone(value), this._deepClone(prevValue), path);
});
}
// Notify general subscribers
this._subscribers.forEach(callback => {
callback(this._deepClone(this._state), prevState);
});
}
// Update multiple paths at once
update(updates) {
const prevState = this._deepClone(this._state);
const changedPaths = [];
Object.entries(updates).forEach(([path, value]) => {
this._setByPath(this._state, path, value);
changedPaths.push(path);
});
// Notify path subscribers for changed paths
changedPaths.forEach(path => {
const pathSubscribers = this._pathSubscribers.get(path);
if (pathSubscribers) {
const currentValue = this._getByPath(this._state, path);
const prevValue = this._getByPath(prevState, path);
pathSubscribers.forEach(callback => {
callback(
this._deepClone(currentValue),
this._deepClone(prevValue),
path
);
});
}
});
// Notify general subscribers
this._subscribers.forEach(callback => {
callback(this._deepClone(this._state), prevState);
});
}
// Subscribe to all changes
subscribe(callback) {
this._subscribers.add(callback);
return () => this._subscribers.delete(callback);
}
// Array helpers
push(path, item) {
const array = this.get(path) || [];
this.set(path, [...array, item]);
}
remove(path, predicate) {
const array = this.get(path) || [];
const filtered = array.filter(item => !predicate(item));
this.set(path, filtered);
}
// Object helpers
merge(path, updates) {
const current = this.get(path) || {};
this.set(path, { ...current, ...updates });
}
}
// Complex app state example
const appStore = new DeepState({
ui: {
modals: {
login: { open: false, data: null },
confirmation: { open: false, data: null }
},
navigation: {
currentPage: 'home',
breadcrumbs: []
},
theme: {
mode: 'light',
primaryColor: '#007bff'
}
},
user: {
profile: null,
preferences: {
notifications: true,
theme: 'auto'
},
permissions: []
},
data: {
posts: [],
comments: {},
categories: []
}
});
// Subscribe to specific UI changes
appStore.subscribeToPath('ui.modals.login', (newValue, prevValue) => {
if (newValue.open && !prevValue.open) {
console.log('Login modal opened');
document.body.classList.add('modal-open');
} else if (!newValue.open && prevValue.open) {
console.log('Login modal closed');
document.body.classList.remove('modal-open');
}
});
// Subscribe to theme changes
appStore.subscribeToPath('ui.theme.mode', (newMode, prevMode) => {
document.documentElement.setAttribute('data-theme', newMode);
});
// Usage examples
appStore.set('ui.modals.login.open', true);
appStore.merge('user.profile', {
name: 'John Doe',
email: 'john@example.com'
});
appStore.push('data.posts', {
id: 1,
title: 'First Post',
content: 'Hello World!'
});
// Batch updates
appStore.update({
'ui.navigation.currentPage': 'profile',
'ui.navigation.breadcrumbs': ['Home', 'Profile'],
'user.preferences.notifications': false
});
Performance Optimization
Efficient State Updates
// Optimized state manager with batching and memoization
class OptimizedState {
constructor(initialState = {}) {
this._state = initialState;
this._subscribers = new Set();
this._updateQueue = [];
this._isUpdating = false;
this._memo = new WeakMap();
}
// Batch multiple updates
batch(updateFn) {
this._isUpdating = true;
const prevState = { ...this._state };
try {
updateFn({
set: (updates) => {
this._updateQueue.push(updates);
}
});
// Apply all queued updates
if (this._updateQueue.length > 0) {
const mergedUpdates = this._updateQueue.reduce((acc, updates) => {
return { ...acc, ...updates };
}, {});
this._state = { ...this._state, ...mergedUpdates };
this._updateQueue = [];
// Notify subscribers once
this._subscribers.forEach(callback => {
callback(this._state, prevState);
});
}
} finally {
this._isUpdating = false;
}
}
// Memoized selector
createSelector(selectorFn, dependencies = []) {
return () => {
const cacheKey = JSON.stringify([
selectorFn.toString(),
...dependencies.map(dep => this._state[dep])
]);
if (this._memo.has(selectorFn) && this._memo.get(selectorFn).key === cacheKey) {
return this._memo.get(selectorFn).value;
}
const result = selectorFn(this._state);
this._memo.set(selectorFn, { key: cacheKey, value: result });
return result;
};
}
// Debounced state updates
createDebouncedUpdater(delay = 300) {
let timeoutId;
return (updates) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
this.setState(updates);
}, delay);
};
}
setState(updates) {
if (this._isUpdating) {
this._updateQueue.push(updates);
return;
}
const prevState = { ...this._state };
this._state = { ...this._state, ...updates };
// Clear memoization cache
this._memo = new WeakMap();
this._subscribers.forEach(callback => {
callback(this._state, prevState);
});
}
getState() {
return this._state;
}
subscribe(callback) {
this._subscribers.add(callback);
return () => this._subscribers.delete(callback);
}
}
// Usage example
const optimizedStore = new OptimizedState({
users: [],
posts: [],
comments: [],
filters: {
searchTerm: '',
category: 'all',
sortBy: 'date'
}
});
// Create memoized selectors
const selectFilteredPosts = optimizedStore.createSelector(
(state) => {
return state.posts.filter(post => {
if (state.filters.category !== 'all' && post.category !== state.filters.category) {
return false;
}
if (state.filters.searchTerm) {
return post.title.toLowerCase().includes(state.filters.searchTerm.toLowerCase());
}
return true;
}).sort((a, b) => {
if (state.filters.sortBy === 'date') {
return new Date(b.createdAt) - new Date(a.createdAt);
}
return a.title.localeCompare(b.title);
});
},
['posts', 'filters'] // Dependencies
);
// Create debounced search updater
const updateSearch = optimizedStore.createDebouncedUpdater(300);
// Batch multiple updates
optimizedStore.batch(({ set }) => {
set({ 'filters.category': 'tech' });
set({ 'filters.sortBy': 'title' });
});
// Usage in search input
document.getElementById('search').addEventListener('input', (e) => {
updateSearch({ 'filters.searchTerm': e.target.value });
});
Local Storage Integration
Persistent State
// State manager with localStorage persistence
class PersistentState {
constructor(key, initialState = {}, options = {}) {
this.storageKey = key;
this.options = {
serialize: JSON.stringify,
deserialize: JSON.parse,
whitelist: null, // Array of keys to persist
blacklist: null, // Array of keys to exclude
version: 1,
migrate: null,
...options
};
this._state = this._loadState(initialState);
this._subscribers = new Set();
// Auto-save on page unload
window.addEventListener('beforeunload', () => {
this._saveState();
});
// Auto-save periodically
setInterval(() => this._saveState(), 30000); // Every 30 seconds
}
_loadState(initialState) {
try {
const serializedState = localStorage.getItem(this.storageKey);
if (!serializedState) {
return { ...initialState };
}
const parsedState = this.options.deserialize(serializedState);
// Version migration
if (parsedState._version !== this.options.version && this.options.migrate) {
const migratedState = this.options.migrate(parsedState, parsedState._version, this.options.version);
return { ...initialState, ...migratedState, _version: this.options.version };
}
return { ...initialState, ...parsedState };
} catch (error) {
console.warn('Failed to load state from localStorage:', error);
return { ...initialState };
}
}
_saveState() {
try {
let stateToSave = { ...this._state, _version: this.options.version };
// Apply whitelist/blacklist
if (this.options.whitelist) {
stateToSave = this.options.whitelist.reduce((filtered, key) => {
if (this._state[key] !== undefined) {
filtered[key] = this._state[key];
}
return filtered;
}, { _version: this.options.version });
}
if (this.options.blacklist) {
this.options.blacklist.forEach(key => {
delete stateToSave[key];
});
}
const serializedState = this.options.serialize(stateToSave);
localStorage.setItem(this.storageKey, serializedState);
} catch (error) {
console.warn('Failed to save state to localStorage:', error);
}
}
getState() {
return { ...this._state };
}
setState(updates) {
const prevState = this.getState();
this._state = { ...this._state, ...updates };
// Save immediately for important updates
this._saveState();
this._subscribers.forEach(callback => {
callback(this.getState(), prevState);
});
}
subscribe(callback) {
this._subscribers.add(callback);
return () => this._subscribers.delete(callback);
}
// Clear persisted state
clear() {
localStorage.removeItem(this.storageKey);
this._state = {};
this._subscribers.forEach(callback => {
callback({}, this._state);
});
}
}
// Usage example with migration
const userPreferences = new PersistentState('userPrefs', {
theme: 'light',
language: 'en',
notifications: true,
sidebarCollapsed: false
}, {
whitelist: ['theme', 'language', 'notifications'], // Don't persist UI state
version: 2,
migrate: (state, fromVersion, toVersion) => {
console.log(`Migrating from version ${fromVersion} to ${toVersion}`);
if (fromVersion === 1 && toVersion === 2) {
// Migration logic from v1 to v2
return {
...state,
theme: state.darkMode ? 'dark' : 'light', // Convert old darkMode boolean
notifications: state.enableNotifications !== false // Default to true
};
}
return state;
}
});
Testing State Management
Unit Testing State Logic
// Test utilities for state management
class StateTestUtils {
static createMockState(initialState = {}) {
const state = new ReactiveState(initialState);
const stateHistory = [state.getState()];
// Track state changes
state.subscribe((newState) => {
stateHistory.push(newState);
});
return {
state,
getHistory: () => [...stateHistory],
getLastState: () => stateHistory[stateHistory.length - 1],
getStateChanges: () => stateHistory.length - 1,
expectState: (expectedState) => {
const current = state.getState();
const matches = Object.keys(expectedState).every(key => {
return JSON.stringify(current[key]) === JSON.stringify(expectedState[key]);
});
if (!matches) {
throw new Error(`State mismatch:\nExpected: ${JSON.stringify(expectedState, null, 2)}\nActual: ${JSON.stringify(current, null, 2)}`);
}
return true;
}
};
}
static async testAsyncAction(actionFn, initialState = {}) {
const mockState = this.createMockState(initialState);
const results = [];
try {
const result = await actionFn(mockState.state);
results.push({ type: 'success', result });
} catch (error) {
results.push({ type: 'error', error });
}
return {
...mockState,
results,
success: results.some(r => r.type === 'success'),
error: results.find(r => r.type === 'error')?.error
};
}
}
// Example tests
function runStateTests() {
console.group('🧪 State Management Tests');
// Test 1: Basic state updates
console.log('Test 1: Basic state updates');
const { state, expectState } = StateTestUtils.createMockState({
count: 0,
user: null
});
state.setState({ count: 5 });
expectState({ count: 5 });
console.log('✅ Basic state updates work');
// Test 2: Computed properties
console.log('Test 2: Computed properties');
const computedState = new ComputedState(
{ items: [] },
{ itemCount: (state) => state.items.length }
);
computedState.setState({ items: ['a', 'b', 'c'] });
const result = computedState.getState();
if (result.itemCount !== 3) {
throw new Error('Computed property failed');
}
console.log('✅ Computed properties work');
// Test 3: Async actions
console.log('Test 3: Async actions');
StateTestUtils.testAsyncAction(
async (state) => {
state.setState({ loading: true });
// Simulate async operation
await new Promise(resolve => setTimeout(resolve, 100));
state.setState({
loading: false,
data: 'test data'
});
return 'success';
},
{ loading: false, data: null }
).then(({ state, success, expectState }) => {
if (!success) throw new Error('Async action failed');
expectState({ loading: false, data: 'test data' });
console.log('✅ Async actions work');
});
console.groupEnd();
}
// Run tests
runStateTests();
Best Practices and Patterns
State Architecture Guidelines
// State management best practices
const StateManagementBestPractices = {
// 1. Single source of truth
createAppStore() {
return new ReactiveState({
// UI state (ephemeral)
ui: {
loading: false,
modals: {},
notifications: []
},
// User data (persistent)
user: {
profile: null,
preferences: {},
permissions: []
},
// Application data (cached)
data: {
posts: [],
categories: [],
cache: {}
}
});
},
// 2. Immutable updates
updateImmutably(state, path, updater) {
const pathArray = path.split('.');
const deepClone = (obj) => JSON.parse(JSON.stringify(obj));
const newState = deepClone(state);
let current = newState;
// Navigate to parent
for (let i = 0; i < pathArray.length - 1; i++) {
current = current[pathArray[i]];
}
// Apply update
const lastKey = pathArray[pathArray.length - 1];
current[lastKey] = updater(current[lastKey]);
return newState;
},
// 3. Normalized state structure
normalizeEntities(entities, idField = 'id') {
return {
byId: entities.reduce((acc, entity) => {
acc[entity[idField]] = entity;
return acc;
}, {}),
allIds: entities.map(entity => entity[idField])
};
},
// 4. Selector patterns
createSelectors(state) {
return {
// Simple selectors
getUser: () => state.user.profile,
isLoading: () => state.ui.loading,
// Parameterized selectors
getPostById: (id) => state.data.posts.find(post => post.id === id),
getPostsByCategory: (category) =>
state.data.posts.filter(post => post.category === category),
// Computed selectors
getActiveUser: () => {
const user = state.user.profile;
return user?.status === 'active' ? user : null;
},
// Memoized expensive computations
getPostsWithCommentCount: (() => {
let cache = null;
let lastPostsLength = 0;
return () => {
if (cache && state.data.posts.length === lastPostsLength) {
return cache;
}
cache = state.data.posts.map(post => ({
...post,
commentCount: state.data.comments[post.id]?.length || 0
}));
lastPostsLength = state.data.posts.length;
return cache;
};
})()
};
}
};
// Usage examples
const appStore = StateManagementBestPractices.createAppStore();
const selectors = StateManagementBestPractices.createSelectors(appStore.getState());
// Normalized data example
const posts = [
{ id: 1, title: 'Post 1', content: '...' },
{ id: 2, title: 'Post 2', content: '...' }
];
const normalizedPosts = StateManagementBestPractices.normalizeEntities(posts);
appStore.setState({ 'data.posts': normalizedPosts });
Framework Integration
React-like Hooks Pattern
// React-inspired hooks for vanilla JavaScript
class VanillaHooks {
constructor() {
this.currentComponent = null;
this.hookIndex = 0;
this.hooks = new WeakMap();
}
withComponent(component, callback) {
const prevComponent = this.currentComponent;
const prevIndex = this.hookIndex;
this.currentComponent = component;
this.hookIndex = 0;
if (!this.hooks.has(component)) {
this.hooks.set(component, []);
}
try {
return callback();
} finally {
this.currentComponent = prevComponent;
this.hookIndex = prevIndex;
}
}
useState(initialValue) {
const component = this.currentComponent;
const index = this.hookIndex++;
const componentHooks = this.hooks.get(component);
if (componentHooks[index] === undefined) {
componentHooks[index] = {
value: initialValue,
setValue: (newValue) => {
const hook = componentHooks[index];
hook.value = typeof newValue === 'function' ? newValue(hook.value) : newValue;
// Trigger re-render
if (component.render) {
component.render();
}
}
};
}
const hook = componentHooks[index];
return [hook.value, hook.setValue];
}
useEffect(callback, dependencies) {
const component = this.currentComponent;
const index = this.hookIndex++;
const componentHooks = this.hooks.get(component);
const hook = componentHooks[index] || {
cleanup: null,
dependencies: null
};
const hasChanged = dependencies === undefined ||
hook.dependencies === null ||
dependencies.some((dep, i) => dep !== hook.dependencies[i]);
if (hasChanged) {
if (hook.cleanup) {
hook.cleanup();
}
hook.cleanup = callback();
hook.dependencies = dependencies ? [...dependencies] : null;
}
componentHooks[index] = hook;
}
useStore(store, selector = (state) => state) {
const [state, setState] = this.useState(selector(store.getState()));
this.useEffect(() => {
return store.subscribe((newState) => {
const selectedState = selector(newState);
setState(selectedState);
});
}, [store, selector]);
return state;
}
}
// Usage example
const hooks = new VanillaHooks();
class TodoComponent {
constructor(container, store) {
this.container = container;
this.store = store;
this.render();
}
render() {
hooks.withComponent(this, () => {
const [newTodo, setNewTodo] = hooks.useState('');
const todos = hooks.useStore(this.store, state => state.todos);
hooks.useEffect(() => {
console.log(`Todo count: ${todos.length}`);
}, [todos.length]);
this.container.innerHTML = `
<div>
<input
typ="text"
placeholde="Add todo..."
valu="${newTodo}"
onchang="this.component.updateNewTodo(this.value)"
/>
<button onclic="this.component.addTodo()">Add</button>
<ul>
${todos.map(todo => `
<li>
<span ${todo.completed ? 'style="text-decoration: line-through"' : ''}>
${todo.text}
</span>
<button onclic="this.component.toggleTodo(${todo.id})">
${todo.completed ? 'Undo' : 'Complete'}
</button>
</li>
`).join('')}
</ul>
</div>
`;
// Store references for event handlers
this.container.querySelectorAll('input, button').forEach(el => {
el.component = this;
});
this.setNewTodo = setNewTodo;
});
}
updateNewTodo(value) {
this.setNewTodo(value);
}
addTodo() {
hooks.withComponent(this, () => {
const [newTodo] = hooks.useState('');
if (newTodo.trim()) {
this.store.dispatch({
type: 'ADD_TODO',
payload: { text: newTodo.trim() }
});
this.setNewTodo('');
}
});
}
toggleTodo(id) {
this.store.dispatch({
type: 'TOGGLE_TODO',
payload: { id }
});
}
}
Conclusion
Vanilla JavaScript state management offers powerful alternatives to library-based solutions. Key benefits include:
Advantages:
- ✅ No dependencies - Reduce bundle size and eliminate version conflicts
- ✅ Full control - Customize behavior exactly to your needs
- ✅ Performance - Optimize for your specific use cases
- ✅ Learning - Deeper understanding of state management concepts
- ✅ Flexibility - Easy integration with any framework or vanilla JS
When to Use Vanilla State Management:
- Small to medium applications
- Performance-critical applications
- Learning projects
- When you need specific behavior not available in libraries
- Projects with strict dependency limitations
Implementation Strategy:
- Start simple - Begin with basic reactive state
- Add complexity gradually - Implement features as needed
- Focus on patterns - Use consistent patterns throughout your app
- Test thoroughly - Validate state logic with unit tests
- Document well - State management logic should be well-documented
Key Patterns to Remember:
- Immutable updates prevent unexpected side effects
- Event-driven architecture enables loose coupling
- Computed properties optimize derived state calculations
- Middleware pattern enables powerful extensibility
- Path-based subscriptions optimize re-rendering
Start with a simple reactive state for your next project and gradually add features as your application grows. You might find that vanilla state management provides all the power you need without the complexity of external libraries.