Advanced State Management
This guide covers advanced state management patterns, optimization techniques, and architectural strategies for complex STX applications.
State Architecture Patterns
Store Composition
typescript
// stores/index.ts
export interface RootState {
auth: AuthState
ui: UIState
data: DataState
features: FeatureState
}
export const rootStore = createStore<RootState>({
modules: {
auth: authModule,
ui: uiModule,
data: dataModule,
features: featureModule
},
plugins: [
persistencePlugin({
key: 'stx-app',
paths: ['auth.user', 'ui.preferences']
}),
devtoolsPlugin({ enabled: __DEV__ }),
loggerPlugin({
collapsed: true,
logActions: true,
logMutations: __DEV__
})
]
})
Module Federation
typescript
// stores/modules/auth.ts
export const authModule = createModule({
namespaced: true,
state: (): AuthState => ({
user: null,
token: null,
permissions: [],
isAuthenticated: false,
loginAttempts: 0,
lastLoginAt: null
}),
getters: {
isAdmin: (state): boolean =>
state.user?.role === 'admin',
hasPermission: (state) => (permission: string): boolean =>
state.permissions.some(p => p.name === permission),
canAccess: (state, getters) => (resource: string): boolean => {
if (getters.isAdmin) return true
return getters.hasPermission(`access:${resource}`)
}
},
mutations: {
SET_USER(state, user: User) {
state.user = user
state.isAuthenticated = !!user
state.lastLoginAt = user ? new Date().toISOString() : null
},
SET_TOKEN(state, token: string) {
state.token = token
},
SET_PERMISSIONS(state, permissions: Permission[]) {
state.permissions = permissions
},
INCREMENT_LOGIN_ATTEMPTS(state) {
state.loginAttempts++
},
RESET_LOGIN_ATTEMPTS(state) {
state.loginAttempts = 0
}
},
actions: {
async login({ commit, dispatch }, credentials: LoginCredentials) {
try {
const response = await authAPI.login(credentials)
commit('SET_USER', response.user)
commit('SET_TOKEN', response.token)
commit('SET_PERMISSIONS', response.permissions)
commit('RESET_LOGIN_ATTEMPTS')
// Set token for API calls
dispatch('setAuthHeader', response.token, { root: true })
return response
} catch (error) {
commit('INCREMENT_LOGIN_ATTEMPTS')
throw error
}
}
}
})
Advanced State Patterns
Event Sourcing
typescript
// stores/event-sourcing.ts
interface Event {
type: string
payload: any
timestamp: number
userId?: string
}
export class EventStore {
private events: Event[] = []
private snapshots = new Map<number, any>()
dispatch(event: Event) {
this.events.push({
...event,
timestamp: Date.now()
})
// Create snapshot every 100 events
if (this.events.length % 100 === 0) {
this.createSnapshot()
}
}
replay(fromTimestamp?: number): any {
const relevantEvents = fromTimestamp
? this.events.filter(e => e.timestamp >= fromTimestamp)
: this.events
return relevantEvents.reduce((state, event) => {
return this.applyEvent(state, event)
}, this.getInitialState())
}
private createSnapshot() {
const currentState = this.replay()
this.snapshots.set(this.events.length, currentState)
}
private applyEvent(state: any, event: Event): any {
switch (event.type) {
case 'USER_CREATED':
return {
...state,
users: [...state.users, event.payload]
}
case 'USER_UPDATED':
return {
...state,
users: state.users.map(user =>
user.id === event.payload.id
? { ...user, ...event.payload.changes }
: user
)
}
default:
return state
}
}
}
CQRS Pattern
typescript
// stores/cqrs.ts
interface Command {
type: string
payload: any
metadata?: any
}
interface Query {
type: string
params?: any
}
export class CQRSStore {
private writeStore = new Map<string, any>()
private readStore = new Map<string, any>()
private projections = new Map<string, (event: Event) => void>()
// Command side (writes)
async execute(command: Command): Promise<void> {
const events = await this.handleCommand(command)
for (const event of events) {
// Apply to write store
this.applyEvent(event)
// Update read projections
this.updateProjections(event)
}
}
// Query side (reads)
query(query: Query): any {
switch (query.type) {
case 'GET_USER_PROFILE':
return this.readStore.get(`user_profile:${query.params.userId}`)
case 'GET_USER_LIST':
return this.readStore.get('user_list') || []
default:
throw new Error(`Unknown query type: ${query.type}`)
}
}
private async handleCommand(command: Command): Promise<Event[]> {
switch (command.type) {
case 'CREATE_USER':
return [{
type: 'USER_CREATED',
payload: command.payload,
timestamp: Date.now()
}]
default:
throw new Error(`Unknown command type: ${command.type}`)
}
}
private updateProjections(event: Event) {
for (const [name, projection] of this.projections) {
try {
projection(event)
} catch (error) {
console.error(`Projection ${name} failed:`, error)
}
}
}
registerProjection(name: string, projection: (event: Event) => void) {
this.projections.set(name, projection)
}
}
State Optimization
Memoized Selectors
typescript
// stores/selectors.ts
import { createSelector } from '@stx/state'
// Basic selector
const getUsers = (state: RootState) => state.data.users
// Memoized selector
const getActiveUsers = createSelector(
[getUsers],
(users) => users.filter(user => user.status === 'active')
)
// Complex memoized selector
const getUsersByRole = createSelector(
[getUsers, (state: RootState, role: string) => role],
(users, role) => users.filter(user => user.role === role)
)
// Selector with multiple dependencies
const getUserStats = createSelector(
[getUsers, getActiveUsers],
(allUsers, activeUsers) => ({
total: allUsers.length,
active: activeUsers.length,
inactive: allUsers.length - activeUsers.length,
percentage: (activeUsers.length / allUsers.length) * 100
})
)
// Usage in components
@component('UserStats')
@computed({
userStats: () => getUserStats(store.state),
adminUsers: () => getUsersByRole(store.state, 'admin')
})
@endcomponent
State Normalization
typescript
// stores/normalization.ts
interface NormalizedState<T> {
byId: Record<string, T>
allIds: string[]
}
export function normalizeEntities<T extends { id: string }>(
entities: T[]
): NormalizedState<T> {
return {
byId: entities.reduce((acc, entity) => {
acc[entity.id] = entity
return acc
}, {} as Record<string, T>),
allIds: entities.map(entity => entity.id)
}
}
export function denormalizeEntities<T>(
normalized: NormalizedState<T>
): T[] {
return normalized.allIds.map(id => normalized.byId[id])
}
// Usage in store
const dataModule = createModule({
state: (): DataState => ({
users: { byId: {}, allIds: [] },
posts: { byId: {}, allIds: [] }
}),
mutations: {
SET_USERS(state, users: User[]) {
state.users = normalizeEntities(users)
},
ADD_USER(state, user: User) {
state.users.byId[user.id] = user
if (!state.users.allIds.includes(user.id)) {
state.users.allIds.push(user.id)
}
},
UPDATE_USER(state, { id, changes }: { id: string, changes: Partial<User> }) {
if (state.users.byId[id]) {
state.users.byId[id] = { ...state.users.byId[id], ...changes }
}
},
REMOVE_USER(state, id: string) {
delete state.users.byId[id]
state.users.allIds = state.users.allIds.filter(userId => userId !== id)
}
},
getters: {
allUsers: (state) => denormalizeEntities(state.users),
getUserById: (state) => (id: string) => state.users.byId[id]
}
})
Async State Management
Resource Management
typescript
// stores/resources.ts
interface ResourceState<T> {
data: T | null
loading: boolean
error: string | null
lastFetch: number | null
stale: boolean
}
export function createResource<T, P = any>(config: {
fetcher: (params: P) => Promise<T>
key: (params: P) => string
staleTime?: number
cacheTime?: number
}) {
const cache = new Map<string, ResourceState<T>>()
return {
async get(params: P): Promise<ResourceState<T>> {
const key = config.key(params)
const existing = cache.get(key)
// Return cached data if fresh
if (existing && !this.isStale(existing)) {
return existing
}
// Set loading state
const loadingState: ResourceState<T> = {
data: existing?.data || null,
loading: true,
error: null,
lastFetch: existing?.lastFetch || null,
stale: false
}
cache.set(key, loadingState)
try {
const data = await config.fetcher(params)
const successState: ResourceState<T> = {
data,
loading: false,
error: null,
lastFetch: Date.now(),
stale: false
}
cache.set(key, successState)
return successState
} catch (error) {
const errorState: ResourceState<T> = {
data: existing?.data || null,
loading: false,
error: error.message,
lastFetch: existing?.lastFetch || null,
stale: true
}
cache.set(key, errorState)
return errorState
}
},
invalidate(params: P) {
const key = config.key(params)
const existing = cache.get(key)
if (existing) {
cache.set(key, { ...existing, stale: true })
}
},
isStale(state: ResourceState<T>): boolean {
if (!state.lastFetch) return true
const staleTime = config.staleTime || 5 * 60 * 1000 // 5 minutes
return Date.now() - state.lastFetch > staleTime
}
}
}
// Usage
const userResource = createResource({
fetcher: (id: string) => userAPI.get(id),
key: (id: string) => `user:${id}`,
staleTime: 5 * 60 * 1000 // 5 minutes
})
@component('UserProfile')
@state({
userState: null as ResourceState<User> | null
})
@mounted(async () => {
userState = await userResource.get(userId)
})
@endcomponent
Optimistic Updates
typescript
// stores/optimistic.ts
interface OptimisticUpdate<T> {
id: string
type: 'create' | 'update' | 'delete'
data: T
rollback: () => void
timestamp: number
}
export class OptimisticManager<T> {
private updates = new Map<string, OptimisticUpdate<T>>()
applyOptimistic(
id: string,
type: 'create' | 'update' | 'delete',
data: T,
rollback: () => void
) {
this.updates.set(id, {
id,
type,
data,
rollback,
timestamp: Date.now()
})
}
confirmUpdate(id: string) {
this.updates.delete(id)
}
rollbackUpdate(id: string) {
const update = this.updates.get(id)
if (update) {
update.rollback()
this.updates.delete(id)
}
}
rollbackAll() {
for (const update of this.updates.values()) {
update.rollback()
}
this.updates.clear()
}
getAll(): OptimisticUpdate<T>[] {
return Array.from(this.updates.values())
}
}
// Usage in store actions
const userModule = createModule({
actions: {
async createUser({ commit, state }, userData: CreateUserData) {
const optimisticId = generateId()
const optimisticUser = { ...userData, id: optimisticId, pending: true }
// Apply optimistic update
commit('ADD_USER', optimisticUser)
try {
const realUser = await userAPI.create(userData)
// Replace optimistic user with real one
commit('REMOVE_USER', optimisticId)
commit('ADD_USER', realUser)
return realUser
} catch (error) {
// Rollback optimistic update
commit('REMOVE_USER', optimisticId)
throw error
}
}
}
})
State Persistence
Advanced Persistence
typescript
// stores/persistence.ts
interface PersistenceConfig {
key: string
paths?: string[]
storage?: Storage
serialize?: (state: any) => string
deserialize?: (stored: string) => any
throttle?: number
encrypt?: boolean
}
export function createPersistencePlugin(config: PersistenceConfig) {
let throttleTimer: number | null = null
return (store: Store) => {
// Load persisted state
const stored = loadState(config)
if (stored) {
store.replaceState(mergeState(store.state, stored))
}
// Subscribe to changes
store.subscribe((mutation, state) => {
if (config.throttle) {
if (throttleTimer) clearTimeout(throttleTimer)
throttleTimer = setTimeout(() => {
saveState(state, config)
}, config.throttle)
} else {
saveState(state, config)
}
})
}
}
function loadState(config: PersistenceConfig): any {
try {
const storage = config.storage || localStorage
const stored = storage.getItem(config.key)
if (!stored) return null
const deserialize = config.deserialize || JSON.parse
let data = deserialize(stored)
if (config.encrypt) {
data = decrypt(data)
}
return data
} catch (error) {
console.error('Failed to load persisted state:', error)
return null
}
}
function saveState(state: any, config: PersistenceConfig) {
try {
const storage = config.storage || localStorage
let dataToSave = config.paths
? pickPaths(state, config.paths)
: state
if (config.encrypt) {
dataToSave = encrypt(dataToSave)
}
const serialize = config.serialize || JSON.stringify
storage.setItem(config.key, serialize(dataToSave))
} catch (error) {
console.error('Failed to save state:', error)
}
}
State Testing
Advanced Testing Patterns
typescript
// tests/store.test.ts
import { createTestStore, StoreTestUtils } from '@stx/testing'
describe('Advanced Store Testing', () => {
let store: Store
let utils: StoreTestUtils
beforeEach(() => {
store = createTestStore({
modules: { user: userModule }
})
utils = new StoreTestUtils(store)
})
test('handles complex async workflows', async () => {
const mockAPI = utils.mockAPI({
'POST /api/users': { delay: 100, response: { id: 1, name: 'John' } },
'GET /api/users/1': { response: { id: 1, name: 'John', posts: [] } }
})
// Start user creation
const createPromise = store.dispatch('user/create', { name: 'John' })
// Should show loading state
expect(store.state.user.creating).toBe(true)
// Wait for creation
const user = await createPromise
expect(store.state.user.creating).toBe(false)
expect(store.state.user.users.byId[user.id]).toEqual(user)
// Verify API calls
expect(mockAPI.history.post).toHaveLength(1)
})
test('state time travel', async () => {
const stateHistory = utils.captureHistory()
store.commit('user/ADD_USER', { id: 1, name: 'John' })
store.commit('user/ADD_USER', { id: 2, name: 'Jane' })
store.commit('user/REMOVE_USER', 1)
expect(stateHistory.length).toBe(4) // initial + 3 mutations
// Travel back to state with both users
utils.restoreState(stateHistory[2])
expect(Object.keys(store.state.user.users.byId)).toHaveLength(2)
})
})
Related Resources
- State Management Guide - Basic state management concepts
- Component State - Component-level state features
- Performance Optimization - State performance patterns
- Testing State - State testing strategies