Skip to content

Latest commit

 

History

History
538 lines (405 loc) · 12.8 KB

File metadata and controls

538 lines (405 loc) · 12.8 KB

Redux Architecture

Overview

The Redux implementation demonstrates a predictable state container with unidirectional data flow. Originally from the JavaScript ecosystem, Redux provides a simple yet powerful pattern for managing application state through explicit actions and pure reducers.

Architecture Diagram

graph TB
    subgraph "View Layer"
        V1[ReduxContentView]
        V2[ReduxSidebarView]
        V3[ReduxContentListView]
        V4[ReduxDetailView]
        V5[ReduxSettingsContentView]
    end

    subgraph "Store"
        S1["ReduxStore
        @Observable"]
        S2["dispatch(_:)
        method"]
    end

    subgraph "State"
        ST1["ReduxAppState
        struct"]
    end

    subgraph "Actions"
        A1["ReduxAction
        enum"]
    end

    subgraph "Reducers"
        R1["appReducer
        pure function"]
    end

    subgraph "Model Layer"
        M1["ReduxListItem
        struct"]
        M2["ReduxSidebarCategory
        enum"]
    end

    V1 -- "reads" --> S1
    V2 -- "dispatch" --> S2
    V3 -- "dispatch" --> S2
    V5 -- "dispatch" --> S2

    S1 -- "holds" --> ST1
    S2 -- "accepts" --> A1
    S2 -- "calls" --> R1

    R1 -- "(state, action)" --> ST1
    R1 -- "returns new" --> ST1
    R1 -- "uses" --> M1
    R1 -- "uses" --> M2

    ST1 -- "contains" --> M1
    ST1 -- "references" --> M2

    style S1 fill:#e7e7ff
    style ST1 fill:#d4edda
    style A1 fill:#fff3cd
    style R1 fill:#f8d7da
    style M1 fill:#fff4e1
    style M2 fill:#fff4e1
Loading

When to Use Redux

Use Redux when:

  • Building apps with complex state management needs
  • You want predictable, explicit state changes
  • Debugging capabilities are important
  • You're familiar with Redux from web development
  • Need time-travel debugging
  • Want testable pure reducers
  • Team prefers functional patterns

Consider alternatives when:

  • Building very simple apps
  • Want minimal boilerplate (use MVVM)
  • Need built-in effects system (use TCA)
  • Team unfamiliar with unidirectional flow

Three Principles of Redux

1. Single Source of Truth

The global state of your application is stored in a single object tree within a single store.

struct ReduxAppState {
    var selectedCategory: ReduxSidebarCategory?
    var selectedItem: ReduxListItem?
    var category1Items: [ReduxListItem]
    // ... all app state here
}

2. State is Read-Only

The only way to change state is to emit an action, an object describing what happened.

// ❌ Wrong
store.state.selectedCategory = .category1

// ✅ Correct
store.dispatch(.selectCategory(.category1))

3. Changes Made with Pure Functions

To specify how the state tree is transformed by actions, you write pure reducers.

func appReducer(state: ReduxAppState, action: ReduxAction) -> ReduxAppState {
    var newState = state
    // Transform state based on action
    return newState
}

Core Concepts

1. State

A single struct containing all application state:

struct ReduxAppState: Equatable, Codable {
    var selectedCategory: ReduxSidebarCategory? = .category1
    var selectedItem: ReduxListItem? = nil
    var category1Items: [ReduxListItem] = []
    var notificationsEnabled: Bool = true
    // ... more state
}

Characteristics:

  • Struct (value type) for immutability
  • Equatable for comparison
  • Codable for persistence (optional)
  • Contains ALL app state

2. Actions

An enum describing every possible state change:

enum ReduxAction {
    case selectCategory(ReduxSidebarCategory?)
    case selectItem(ReduxListItem?)
    case addItem
    case deleteItem(ReduxListItem)
    case toggleNotifications(Bool)
}

Characteristics:

  • Enum cases represent events
  • Associated values carry data
  • Describes WHAT happened, not HOW
  • Serializable for logging/debugging

3. Reducers

Pure functions that transform state:

func appReducer(state: ReduxAppState, action: ReduxAction) -> ReduxAppState {
    var newState = state

    switch action {
    case let .selectCategory(category):
        newState.selectedCategory = category
        newState.selectedItem = nil
    // ... handle other actions
    }

    return newState
}

Characteristics:

  • Pure function (no side effects)
  • Same input always produces same output
  • Returns NEW state, doesn't mutate input
  • Testable in isolation

4. Store

The runtime that holds state and accepts actions:

@Observable
class ReduxStore {
    private(set) var state: ReduxAppState

    func dispatch(_ action: ReduxAction) {
        state = reducer(state, action)
    }
}

Characteristics:

  • Holds current state
  • Provides dispatch method
  • Calls reducer on dispatch
  • Notifies subscribers of changes

Unidirectional Data Flow

sequenceDiagram
    actor User
    participant View
    participant Store
    participant Reducer
    participant State

    User->>View: Taps "Add Item"
    View->>Store: dispatch(.addItem)
    Store->>Reducer: appReducer(state, .addItem)
    Reducer->>Reducer: Create new state
    Reducer-->>Store: Return new state
    Store->>State: Update state
    Store-->>View: Notify observers
    View-->>User: UI updates
Loading

File Structure

Examples/Redux/
├── Models/                         # Pure data structures
│   ├── ReduxSidebarCategory.swift  # Category enum
│   └── ReduxListItem.swift         # Item struct
│
├── State/                          # Application state
│   └── ReduxAppState.swift         # Global state struct
│
├── Actions/                        # Action definitions
│   └── ReduxActions.swift          # All actions enum
│
├── Reducers/                       # State transformation
│   └── ReduxReducers.swift         # Pure reducer functions
│
├── Store/                          # Redux store
│   └── ReduxStore.swift            # Store implementation
│
└── Views/                          # SwiftUI views
    ├── ReduxContentView.swift      # Root view
    ├── ReduxSidebarView.swift      # Sidebar (first pane)
    ├── ReduxContentListView.swift  # Content list (second pane)
    ├── ReduxDetailView.swift       # Detail (third pane)
    └── ReduxSettingsContentView.swift # Settings form

Layer Details

1. Models (Data)

Purpose: Pure data structures

struct ReduxListItem: Identifiable, Hashable, Codable {
    let id = UUID()
    let title: String
    let subtitle: String
    // ... more properties
}

Characteristics:

  • Immutable (let)
  • Codable for state persistence
  • No business logic

2. State (Application State)

Purpose: Single source of truth

struct ReduxAppState: Equatable, Codable {
    var selectedCategory: ReduxSidebarCategory? = .category1
    var selectedItem: ReduxListItem? = nil
    var category1Items: [ReduxListItem] = []
    // ... all state properties
}

Characteristics:

  • One struct for entire app
  • Equatable for change detection
  • Codable for persistence
  • Organized by feature/domain

3. Actions (Events)

Purpose: Describe state changes

enum ReduxAction {
    case selectCategory(ReduxSidebarCategory?)
    case addItem
    case deleteItem(ReduxListItem)
}

Characteristics:

  • Enum for type safety
  • Associated values for data
  • Past tense naming
  • Serializable

4. Reducers (Transformers)

Purpose: Transform state based on actions

func appReducer(state: ReduxAppState, action: ReduxAction) -> ReduxAppState {
    var newState = state
    // Transform based on action
    return newState
}

Characteristics:

  • Pure function
  • No side effects
  • Creates new state
  • Composable (can split into smaller reducers)

5. Store (Runtime)

Purpose: Manage state and dispatch actions

@Observable
class ReduxStore {
    private(set) var state: ReduxAppState

    func dispatch(_ action: ReduxAction) {
        state = reducer(state, action)
    }
}

Characteristics:

  • @Observable for SwiftUI
  • Private(set) enforces read-only state
  • Dispatch is only mutation method
  • @MainActor for thread safety

6. Views (UI)

Purpose: Display state and dispatch actions

struct ReduxContentView: View {
    let store: ReduxStore

    var body: some View {
        Button("Add") {
            store.dispatch(.addItem)
        }
    }
}

Characteristics:

  • Read from store.state
  • Dispatch actions on user interaction
  • No business logic
  • Automatic updates via @Observable

Testing

Redux makes testing straightforward with pure functions:

func testSelectCategory() {
    let initialState = ReduxAppState()
    let newState = appReducer(
        state: initialState,
        action: .selectCategory(.category2)
    )

    XCTAssertEqual(newState.selectedCategory, .category2)
    XCTAssertNil(newState.selectedItem)
}

Benefits

1. Predictability

Every state change is explicit through actions:

store.dispatch(.selectCategory(.category1))

No hidden mutations or side effects.

2. Debugging

Action log provides complete app history:

📤 Action: selectCategory(.category1)
📦 New State: Selected Category = Category 1
📤 Action: addItem
📦 New State: Category 1 items count = 4

3. Testability

Reducers are pure functions:

let result = appReducer(state, action)
XCTAssertEqual(result.selectedCategory, .category1)

4. Time Travel

Can implement undo/redo easily:

var stateHistory: [ReduxAppState] = []

func dispatch(_ action: ReduxAction) {
    stateHistory.append(state)
    state = reducer(state, action)
}

func undo() {
    state = stateHistory.popLast() ?? state
}

Comparison with Other Patterns

Aspect Redux TCA MVVM VIPER
Complexity Medium High Medium High
Testability Excellent Excellent Good Excellent
Learning Curve Medium Steep Medium Steep
Boilerplate Medium High Low High
State Flow Unidirect. Unidirect. Bidir. Unidirect.
Dependencies Optional Required None None
Effects System Manual Built-in Manual Manual
Web Familiarity High Low Medium Low

Redux vs TCA

Feature Redux TCA
Library Optional (ReSwift) Required
Effects Manual Built-in
Composition Manual First-class
Dependencies Manual injection Built-in system
Testing Manual setup TestStore provided
Boilerplate Moderate High
Learning Curve Medium Steep
Simplicity High Medium

When to choose Redux: Simpler apps, familiarity with web Redux, want minimal dependencies.

When to choose TCA: Complex apps, heavy effects/async, want comprehensive tooling.

Best Practices

  1. Keep State Normalized - Avoid nested/duplicated data
  2. Pure Reducers - No side effects, API calls, or mutations
  3. Descriptive Actions - Names explain what happened
  4. Compose Reducers - Break large reducers into domain-specific ones
  5. Type Safety - Use enums for actions, structs for state
  6. Test Reducers - Easy since they're pure functions
  7. Single Store - Don't create multiple stores

Reducer Composition

Large apps should split reducers by domain:

func appReducer(state: ReduxAppState, action: ReduxAction) -> ReduxAppState {
    var newState = state

    newState = navigationReducer(state: newState, action: action)
    newState = itemsReducer(state: newState, action: action)
    newState = settingsReducer(state: newState, action: action)

    return newState
}

Each domain reducer handles its own slice of state.

Learning Resources

Summary

Redux provides a predictable state container with:

  • ✅ Unidirectional data flow
  • ✅ Single source of truth
  • ✅ Pure reducers for testability
  • ✅ Explicit state changes
  • ✅ Time-travel debugging capabilities
  • ✅ No external dependencies (in this implementation)

Trade-off: More boilerplate than MVVM, less tooling than TCA.

Recommendation: Use for apps needing predictable state management without the complexity of TCA.