A production reducer can grow beyond one readable file. Splitting it poorly is worse: actions become hard to find and the feature's flow disappears across extensions.
I keep one conceptual feature while separating predictable pieces:
WorkoutFeature.swift
WorkoutFeature+State.swift
WorkoutFeature+Action.swift
WorkoutFeature+Reducer.swift
WorkoutFeature+Navigation.swift
The root declares dependencies and composes child reducers. State and action files define the public vocabulary. Reducer extensions group behavior by flow: loading, editing, saving, or live execution.
Extract a child feature when it has its own lifecycle, state machine, cancellation, or presentation. Do not extract a child merely because a view is visually reusable.
@Reducer
struct WorkoutFeature {
@ObservableState
struct State: Equatable {
var session: SessionFeature.State?
@Presents var destination: Destination.State?
}
}
Actions should still reveal causality. Prefer saveResponse(Result<...>) to a generic setLoading(false). Grouping code must not weaken the event model.
Private pure helpers can move into domain files. API mapping belongs in data implementations. This keeps the reducer focused on transitions rather than becoming the only place code is allowed to exist.
The split succeeds when a reader can begin at the root, understand the state and action vocabulary, and follow one flow without opening unrelated files.