Build your own state management library to understand how popular solutions like Redux and Vuex work under the hood. 🏗️
Understanding State Management 🎯
State management libraries help maintain application state in a predictable way. Let's create our own implementation with features similar to popular solutions.
Basic Store Implementation
Core Store Class
class Store {
constructor(initialState = {}) {
this.state = initialState;
this.listeners = new Set();
this.reducers = new Map();
}
getState() {
return this.state;
}
subscribe(listener) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
dispatch(action) {
const reducer = this.reducers.get(action.type);
if (reducer) {
this.state = reducer(this.state, action);
this.notify();
}
return action;
}
notify() {
this.listeners.forEach(listener => listener(this.state));
}
}
Basic Usage
const store = new Store({ count: 0 });
store.reducers.set('INCREMENT', (state, action) => ({
...state,
count: state.count + 1
}));
store.subscribe(state => {
console.log('State updated:', state);
});
store.dispatch({ type: 'INCREMENT' });
Advanced Features 🌟
1. Middleware Support
class Store {
constructor(initialState = {}) {
this.state = initialState;
this.listeners = new Set();
this.reducers = new Map();
this.middlewares = [];
}
use(middleware) {
this.middlewares.push(middleware);
return this;
}
dispatch(action) {
let next = (action) => {
const reducer = this.reducers.get(action.type);
if (reducer) {
this.state = reducer(this.state, action);
this.notify();
}
return action;
};
const chain = this.middlewares
.map(middleware => middleware(this))
.reverse()
.reduce((composed, middleware) => {
return (action) => middleware(composed)(action);
}, next);
return chain(action);
}
}
2. Time Travel Debugging
class TimeTravel {
constructor(store) {
this.store = store;
this.history = [store.getState()];
this.currentIndex = 0;
store.subscribe(state => {
if (this.currentIndex === this.history.length - 1) {
this.history.push({ ...state });
this.currentIndex++;
}
});
}
undo() {
if (this.currentIndex > 0) {
this.currentIndex--;
this.store.state = this.history[this.currentIndex];
this.store.notify();
}
}
redo() {
if (this.currentIndex < this.history.length - 1) {
this.currentIndex++;
this.store.state = this.history[this.currentIndex];
this.store.notify();
}
}
}
3. Computed Properties
class Store {
constructor(initialState = {}) {
this.state = initialState;
this.computed = new Map();
this.computedCache = new Map();
}
addComputed(name, computation) {
this.computed.set(name, computation);
this.updateComputed(name);
}
updateComputed(name) {
const computation = this.computed.get(name);
if (computation) {
this.computedCache.set(name, computation(this.state));
}
}
getComputed(name) {
return this.computedCache.get(name);
}
}
Real-World Implementation 💡
Complete Store Implementation
class Store {
constructor(options = {}) {
this.state = options.state || {};
this.listeners = new Set();
this.reducers = new Map();
this.middlewares = [];
this.computed = new Map();
this.computedCache = new Map();
// Initialize with provided reducers
if (options.reducers) {
Object.entries(options.reducers).forEach(([type, reducer]) => {
this.reducers.set(type, reducer);
});
}
// Add computed properties
if (options.computed) {
Object.entries(options.computed).forEach(([name, computation]) => {
this.addComputed(name, computation);
});
}
}
// State access
getState() {
return this.state;
}
// Subscription management
subscribe(listener) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
// Action dispatch
dispatch(action) {
let next = (action) => {
const reducer = this.reducers.get(action.type);
if (reducer) {
this.state = reducer(this.state, action);
this.updateAllComputed();
this.notify();
}
return action;
};
const chain = this.middlewares
.map(middleware => middleware(this))
.reverse()
.reduce((composed, middleware) => {
return (action) => middleware(composed)(action);
}, next);
return chain(action);
}
// Middleware support
use(middleware) {
this.middlewares.push(middleware);
return this;
}
// Computed properties
addComputed(name, computation) {
this.computed.set(name, computation);
this.updateComputed(name);
}
updateComputed(name) {
const computation = this.computed.get(name);
if (computation) {
this.computedCache.set(name, computation(this.state));
}
}
updateAllComputed() {
this.computed.forEach((_, name) => this.updateComputed(name));
}
getComputed(name) {
return this.computedCache.get(name);
}
// Notification system
notify() {
this.listeners.forEach(listener => listener(this.state));
}
}
Usage Example
// Create store
const store = new Store({
state: {
todos: [],
filter: 'all'
},
reducers: {
ADD_TODO: (state, action) => ({
...state,
todos: [...state.todos, action.payload]
}),
TOGGLE_TODO: (state, action) => ({
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
)
}),
SET_FILTER: (state, action) => ({
...state,
filter: action.payload
})
},
computed: {
activeTodos: state =>
state.todos.filter(todo => !todo.completed),
completedTodos: state =>
state.todos.filter(todo => todo.completed),
filteredTodos: state => {
switch (state.filter) {
case 'active':
return state.todos.filter(todo => !todo.completed);
case 'completed':
return state.todos.filter(todo => todo.completed);
default:
return state.todos;
}
}
}
});
// Add logging middleware
store.use(store => next => action => {
console.log('Dispatching:', action);
const result = next(action);
console.log('New state:', store.getState());
return result;
});
// Subscribe to changes
store.subscribe(state => {
console.log('Active todos:', store.getComputed('activeTodos'));
console.log('Completed todos:', store.getComputed('completedTodos'));
});
// Dispatch actions
store.dispatch({
type: 'ADD_TODO',
payload: { id: 1, text: 'Learn state management', completed: false }
});
Best Practices 📝
- Immutable state updates
- Single source of truth
- Predictable state changes
- Clear action types
- Minimal state shape
- Computed properties for derived data
- Middleware for side effects
- Error handling
- Performance optimization
- Type safety (with TypeScript)
Performance Optimization 🚀
1. Selective Updates
class Store {
subscribe(listener, selector = state => state) {
let lastSelected = selector(this.state);
const wrappedListener = (state) => {
const selected = selector(state);
if (selected !== lastSelected) {
lastSelected = selected;
listener(selected);
}
};
this.listeners.add(wrappedListener);
return () => this.listeners.delete(wrappedListener);
}
}
2. Batched Updates
class Store {
constructor() {
this.updateScheduled = false;
// ... other initialization
}
notify() {
if (!this.updateScheduled) {
this.updateScheduled = true;
Promise.resolve().then(() => {
this.updateScheduled = false;
this.listeners.forEach(listener => listener(this.state));
});
}
}
}
Additional Resources
Building your own state management library helps understand the core concepts behind popular solutions. While you might not use this in production, the knowledge gained will make you better at using existing libraries.