Skip to content

A pattern that separates state management from component's definition which keeps concerns well separated leading to easily maintainable code.

Notifications You must be signed in to change notification settings

hamdi4-beep/state-management

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Custom State Management Pattern

A lightweight React state management implementation demonstrating clean separation of concerns through normalized state, semantic actions, and layered architecture. Built as an educational exploration of solving state management at the component-dispatcher boundary without external dependencies.

Overview

This codebase implements a custom state management pattern that completely decouples components from state logic. Components interact through a semantic interface that hides all implementation details – state structure, action types, and transformation logic remain invisible to the presentation layer.

Architecture

The system uses a layered architecture where each module has a single, well-defined responsibility:

  • Components (App.jsx) - Purely presentational logic, no knowledge of state structure or action types
  • Hooks (hooks.js) - Semantic interface exposing state slices and action methods
  • Dispatchers (dispatchers.js) - Pure functions handling state transformations
  • Reducer (reducer.js) - Thin delegation layer routing actions to appropriate dispatchers
  • Provider (Provider.jsx) - Context wrapper managing state and dispatch distribution

Key Technical Achievements

1. Semantic Action Interface

Actions are expressed as domain-specific methods (itemCreated, itemUpdated, itemDeleted) rather than raw dispatch calls. Components describe what they want to happen, not how it happens. This abstraction allows the underlying action structure (types, payloads) to change without touching component code.

2. Normalized State Design

State uses a byId/allIds pattern for efficient lookups and updates:

{
  byId: { 1: { id: 1, value: "Item 1" }, 2: { ... } },
  allIds: [1, 2]
}

This eliminates O(n) lookups, prevents data duplication, and enables direct entity access by ID.

3. Dispatcher Registry Pattern

Dispatchers are organized as a pluggable object registry rather than a monolithic switch statement. New operations are added by creating dispatcher functions and registering them by key, enabling extensibility without modifying existing code.

4. Complete Decoupling Through Hooks

Components have zero awareness of:

  • Context implementation details
  • State shape or structure
  • Action types or payload formats
  • Dispatcher existence or implementation

The entire state management system could be swapped out by rewriting only the hooks – components remain unchanged.

5. Immutable Updates with Reference Preservation

State updates use Object.assign({}, ...) to create new objects only where necessary. Unchanged portions of state maintain their references, allowing React to skip reconciliation for components using unmodified data.

6. Unidirectional Data Flow

State flows down through context, actions flow up through dispatch. No two-way binding or direct mutations exist anywhere in the system. This makes state changes predictable and traceable.

File Structure

src/
├── App.jsx          # Main application with component examples
├── Provider.jsx     # Context provider wrapping useReducer
├── hooks.js         # useActions, useItem, useAllItemIds
├── dispatchers.js   # ADD_ITEM, UPDATE_ITEM, DELETE_ITEM handlers
├── reducer.js       # Dispatcher delegation logic
├── data.js          # Initial normalized state
└── main.jsx         # Application entry point

Usage Example

import { useActions, useItem } from './hooks'

function Component({ id }) {
  const item = useItem(id)
  const actions = useActions()

  const handleUpdate = () => 
    actions.itemUpdated(id, 'New value')

  return (
    <div>
      <p>{item.value}</p>
      <button onClick={handleUpdate}>Update</button>
      <button onClick={() => actions.itemDeleted(id)}>Delete</button>
    </div>
  )
}

Components consume a clean API without knowing:

  • That actions dispatch { type: 'UPDATE_ITEM', payload: {...} }
  • That state is normalized with byId/allIds
  • That dispatchers exist or how they transform state

Design Principles

  1. Separation of Concerns: Each module handles one responsibility
  2. Semantic Abstraction: Actions describe business intent, not technical operations
  3. Hidden Implementation: Components never import dispatchers or action types
  4. Pure Transformations: Dispatchers are pure functions with no side effects
  5. Stable Contracts: Hook signatures provide unchanging API boundaries
  6. Explicit Flow: State changes only occur through explicit dispatch calls

Benefits

  • Maintainability: Changes to state logic don't cascade to components
  • Testability: Dispatchers are pure functions easily tested in isolation
  • Scalability: Normalized structure and reference preservation support growing datasets
  • Extensibility: New operations added without modifying existing code
  • Predictability: Unidirectional flow makes debugging straightforward
  • Flexibility: Internal implementation can evolve independently from component code

Technical Considerations

Why Not Redux?

This pattern solves state management at the component-dispatcher boundary rather than bringing in a full state management library. It provides the core benefits of Redux (predictable updates, separation of concerns) without the overhead of middleware, DevTools integration, or time-travel debugging.

Performance Characteristics

Context re-renders all consumers when any part of state changes. For applications with many components and frequent updates, consider:

  • Splitting context into multiple providers for different state domains
  • Using selectors with useMemo for derived state
  • Implementing React.memo for expensive component trees

The current architecture prioritizes clarity and maintainability over premature optimization.

When to Use This Pattern

Appropriate for:

  • Learning how state management patterns work internally
  • Small to medium applications with straightforward state needs
  • Projects where external dependencies should be minimized
  • Teams wanting full control over state management implementation

Consider alternatives for:

  • Large applications needing DevTools or middleware ecosystems
  • Complex async workflows requiring sophisticated side effect handling
  • Applications with performance-critical frequent updates across many components

Educational Purpose

This implementation demonstrates solving state management at the appropriate abstraction level. Rather than reaching for external libraries, it shows how to build architectural boundaries that keep components focused on presentation while state logic remains isolated and testable.

The patterns here – normalized state, semantic actions, dispatcher registries, and hook-based abstractions – are transferable concepts that apply regardless of the specific state management tool used in production.

About

A pattern that separates state management from component's definition which keeps concerns well separated leading to easily maintainable code.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published