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.
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
✅ 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
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
}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))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
}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
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
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
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
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
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
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
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
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
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)
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
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
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)
}Every state change is explicit through actions:
store.dispatch(.selectCategory(.category1))No hidden mutations or side effects.
Action log provides complete app history:
📤 Action: selectCategory(.category1)
📦 New State: Selected Category = Category 1
📤 Action: addItem
📦 New State: Category 1 items count = 4
Reducers are pure functions:
let result = appReducer(state, action)
XCTAssertEqual(result.selectedCategory, .category1)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
}| 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 |
| 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.
- Keep State Normalized - Avoid nested/duplicated data
- Pure Reducers - No side effects, API calls, or mutations
- Descriptive Actions - Names explain what happened
- Compose Reducers - Break large reducers into domain-specific ones
- Type Safety - Use enums for actions, structs for state
- Test Reducers - Easy since they're pure functions
- Single Store - Don't create multiple stores
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.
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.