Skip to content

Commit fe19a30

Browse files
stephencelismbrandonw
authored andcommitted
More docs around Reducer.combine and ordering (#215)
* More docs around Reducer.combine and ordering * typo * Update Sources/ComposableArchitecture/Reducer.swift Co-authored-by: Brandon Williams <[email protected]> * more * Update Reducer.swift * more Co-authored-by: Brandon Williams <[email protected]>
1 parent 2f02d93 commit fe19a30

File tree

1 file changed

+164
-12
lines changed

1 file changed

+164
-12
lines changed

Sources/ComposableArchitecture/Reducer.swift

Lines changed: 164 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,99 @@ public struct Reducer<State, Action, Environment> {
5555
Self { _, _, _ in .none }
5656
}
5757

58-
/// Combines many reducers into a single one by running each one on the state, and merging
58+
/// Combines many reducers into a single one by running each one on state in order, and merging
5959
/// all of the effects.
6060
///
61+
/// It is important to note that the order of combining reducers matter. Combining `reducerA` with
62+
/// `reducerB` is not necessarily the same as combining `reducerB` with `reducerA`.
63+
///
64+
/// This can become an issue when working with reducers that have overlapping domains. For
65+
/// example, if `reducerA` embeds the domain of `reducerB` and reacts to its actions or modifies
66+
/// its state, it can make a difference if `reducerA` chooses to modify `reducerB`'s state
67+
/// _before_ or _after_ `reducerB` runs.
68+
///
69+
/// This is perhaps most easily seen when working with `optional` reducers, where the parent
70+
/// domain may listen to the child domain and `nil` out its state. If the parent reducer runs
71+
/// before the child reducer, then the child reducer will not be able to react to its own action.
72+
///
73+
/// Similar can be said for a `forEach` reducer. If the parent domain modifies the child
74+
/// collection by moving, removing, or modifying an element before the `forEach` reducer runs, the
75+
/// `forEach` reducer may perform its action against the wrong element, an element that no longer
76+
/// exists, or an element in an unexpected state.
77+
///
78+
/// Running a parent reducer before a child reducer can be considered an application logic
79+
/// error, and can produce assertion failures. So you should almost always combine reducers in
80+
/// order from child to parent domain.
81+
///
82+
/// Here is an example of how you should combine an `optional` reducer with a parent domain:
83+
///
84+
/// let parentReducer = Reducer<ParentState, ParentAction, ParentEnvironment>.combine(
85+
/// // Combined before parent so that it can react to `.dismiss` while state is non-`nil`.
86+
/// childReducer.optional.pullback(
87+
/// state: \.child,
88+
/// action: /ParentAction.child,
89+
/// environment: { $0.child }
90+
/// ),
91+
/// // Combined after child so that it can `nil` out child state upon `.child(.dismiss)`.
92+
/// Reducer { state, action, environment in
93+
/// switch action
94+
/// case .child(.dismiss):
95+
/// state.child = nil
96+
/// return .none
97+
/// ...
98+
/// }
99+
/// },
100+
/// )
101+
///
61102
/// - Parameter reducers: A list of reducers.
62103
/// - Returns: A single reducer.
63104
public static func combine(_ reducers: Reducer...) -> Reducer {
64105
.combine(reducers)
65106
}
66107

67-
/// Combines an array of reducers into a single one by running each one on the state, and
68-
/// merging all of the effects.
108+
/// Combines many reducers into a single one by running each one on state in order, and merging
109+
/// all of the effects.
110+
///
111+
/// It is important to note that the order of combining reducers matter. Combining `reducerA` with
112+
/// `reducerB` is not necessarily the same as combining `reducerB` with `reducerA`.
113+
///
114+
/// This can become an issue when working with reducers that have overlapping domains. For
115+
/// example, if `reducerA` embeds the domain of `reducerB` and reacts to its actions or modifies
116+
/// its state, it can make a difference if `reducerA` chooses to modify `reducerB`'s state
117+
/// _before_ or _after_ `reducerB` runs.
118+
///
119+
/// This is perhaps most easily seen when working with `optional` reducers, where the parent
120+
/// domain may listen to the child domain and `nil` out its state. If the parent reducer runs
121+
/// before the child reducer, then the child reducer will not be able to react to its own action.
122+
///
123+
/// Similar can be said for a `forEach` reducer. If the parent domain modifies the child
124+
/// collection by moving, removing, or modifying an element before the `forEach` reducer runs, the
125+
/// `forEach` reducer may perform its action against the wrong element, an element that no longer
126+
/// exists, or an element in an unexpected state.
127+
///
128+
/// Running a parent reducer before a child reducer can be considered an application logic
129+
/// error, and can produce assertion failures. So you should almost always combine reducers in
130+
/// order from child to parent domain.
131+
///
132+
/// Here is an example of how you should combine an `optional` reducer with a parent domain:
133+
///
134+
/// let parentReducer = Reducer<ParentState, ParentAction, ParentEnvironment>.combine(
135+
/// // Combined before parent so that it can react to `.dismiss` while state is non-`nil`.
136+
/// childReducer.optional.pullback(
137+
/// state: \.child,
138+
/// action: /ParentAction.child,
139+
/// environment: { $0.child }
140+
/// ),
141+
/// // Combined after child so that it can `nil` out child state upon `.child(.dismiss)`.
142+
/// Reducer { state, action, environment in
143+
/// switch action
144+
/// case .child(.dismiss):
145+
/// state.child = nil
146+
/// return .none
147+
/// ...
148+
/// }
149+
/// },
150+
/// )
69151
///
70152
/// - Parameter reducers: An array of reducers.
71153
/// - Returns: A single reducer.
@@ -75,8 +157,52 @@ public struct Reducer<State, Action, Environment> {
75157
}
76158
}
77159

78-
/// Combines the current reducer with another given reducer by running each one on the state,
79-
/// and merging their effects.
160+
/// Combines many reducers into a single one by running each one on state in order, and merging
161+
/// all of the effects.
162+
///
163+
/// It is important to note that the order of combining reducers matter. Combining `reducerA` with
164+
/// `reducerB` is not necessarily the same as combining `reducerB` with `reducerA`.
165+
///
166+
/// This can become an issue when working with reducers that have overlapping domains. For
167+
/// example, if `reducerA` embeds the domain of `reducerB` and reacts to its actions or modifies
168+
/// its state, it can make a difference if `reducerA` chooses to modify `reducerB`'s state
169+
/// _before_ or _after_ `reducerB` runs.
170+
///
171+
/// This is perhaps most easily seen when working with `optional` reducers, where the parent
172+
/// domain may listen to the child domain and `nil` out its state. If the parent reducer runs
173+
/// before the child reducer, then the child reducer will not be able to react to its own action.
174+
///
175+
/// Similar can be said for a `forEach` reducer. If the parent domain modifies the child
176+
/// collection by moving, removing, or modifying an element before the `forEach` reducer runs, the
177+
/// `forEach` reducer may perform its action against the wrong element, an element that no longer
178+
/// exists, or an element in an unexpected state.
179+
///
180+
/// Running a parent reducer before a child reducer can be considered an application logic
181+
/// error, and can produce assertion failures. So you should almost always combine reducers in
182+
/// order from child to parent domain.
183+
///
184+
/// Here is an example of how you should combine an `optional` reducer with a parent domain:
185+
///
186+
/// let parentReducer: Reducer<ParentState, ParentAction, ParentEnvironment> =
187+
/// // Run before parent so that it can react to `.dismiss` while state is non-`nil`.
188+
/// childReducer
189+
/// .optional
190+
/// .pullback(
191+
/// state: \.child,
192+
/// action: /ParentAction.child,
193+
/// environment: { $0.child }
194+
/// )
195+
/// // Combined after child so that it can `nil` out child state upon `.child(.dismiss)`.
196+
/// .combined(
197+
/// with: Reducer { state, action, environment in
198+
/// switch action
199+
/// case .child(.dismiss):
200+
/// state.child = nil
201+
/// return .none
202+
/// ...
203+
/// }
204+
/// }
205+
/// )
80206
///
81207
/// - Parameter other: Another reducer.
82208
/// - Returns: A single reducer.
@@ -140,7 +266,8 @@ public struct Reducer<State, Action, Environment> {
140266
/// only running the non-optional reducer when state is non-nil.
141267
///
142268
/// Often used in tandem with `pullback` to transform a reducer on a non-optional local domain
143-
/// into a reducer on a global domain that contains an optional local domain:
269+
/// into a reducer that can be combined with a reducer on a global domain that contains some
270+
/// optional local domain:
144271
///
145272
/// // Global domain that holds an optional local domain:
146273
/// struct AppState { var modal: ModalState? }
@@ -151,12 +278,20 @@ public struct Reducer<State, Action, Environment> {
151278
/// let modalReducer = Reducer<ModalState, ModalAction, ModalEnvironment { ... }
152279
///
153280
/// // Pullback the local modal reducer so that it works on all of the app domain:
154-
/// let appReducer: Reducer<AppState, AppAction, AppEnvironment> =
281+
/// let appReducer = Reducer<AppState, AppAction, AppEnvironment>.combine(
155282
/// modalReducer.optional().pullback(
156283
/// state: \.modal,
157284
/// action: /AppAction.modal,
158285
/// environment: { ModalEnvironment(mainQueue: $0.mainQueue) }
159-
/// )
286+
/// ),
287+
/// Reducer { state, action, environment in
288+
/// ...
289+
/// }
290+
/// )
291+
///
292+
/// Take care when combining optional reducers into parent domains, as order matters. Always
293+
/// combine optional reducers _before_ parent reducers that can `nil` out the associated optional
294+
/// state.
160295
///
161296
/// - See also: `IfLetStore`, a SwiftUI helper for transforming a store on optional state into a
162297
/// store on non-optional state.
@@ -205,12 +340,19 @@ public struct Reducer<State, Action, Environment> {
205340
/// let todoReducer = Reducer<Todo, TodoAction, TodoEnvironment> { ... }
206341
///
207342
/// // Pullback the local todo reducer so that it works on all of the app domain:
208-
/// let appReducer: Reducer<AppState, AppAction, AppEnvironment> =
343+
/// let appReducer = Reducer<AppState, AppAction, AppEnvironment>.combine(
209344
/// todoReducer.forEach(
210345
/// state: \.todos,
211346
/// action: /AppAction.todo(index:action:),
212347
/// environment: { _ in TodoEnvironment() }
213-
/// )
348+
/// ),
349+
/// Reducer { state, action, environment in
350+
/// ...
351+
/// }
352+
/// )
353+
///
354+
/// Take care when combining `forEach` reducers into parent domains, as order matters. Always
355+
/// combine `forEach` reducers _before_ parent reducers that can modify the collection.
214356
///
215357
/// - Parameters:
216358
/// - toLocalState: A key path that can get/set an array of `State` elements inside.
@@ -276,12 +418,19 @@ public struct Reducer<State, Action, Environment> {
276418
/// let todoReducer = Reducer<Todo, TodoAction, TodoEnvironment> { ... }
277419
///
278420
/// // Pullback the local todo reducer so that it works on all of the app domain:
279-
/// let appReducer: Reducer<AppState, AppAction, AppEnvironment> =
421+
/// let appReducer = Reducer<AppState, AppAction, AppEnvironment>.combine(
280422
/// todoReducer.forEach(
281423
/// state: \.todos,
282424
/// action: /AppAction.todo(id:action:),
283425
/// environment: { _ in TodoEnvironment() }
284-
/// )
426+
/// ),
427+
/// Reducer { state, action, environment in
428+
/// ...
429+
/// }
430+
/// )
431+
///
432+
/// Take care when combining `forEach` reducers into parent domains, as order matters. Always
433+
/// combine `forEach` reducers _before_ parent reducers that can modify the collection.
285434
///
286435
/// - Parameters:
287436
/// - toLocalState: A key path that can get/set a collection of `State` elements inside
@@ -342,6 +491,9 @@ public struct Reducer<State, Action, Environment> {
342491
/// A version of `pullback` that transforms a reducer that works on an element into one that works
343492
/// on a dictionary of element values.
344493
///
494+
/// Take care when combining `forEach` reducers into parent domains, as order matters. Always
495+
/// combine `forEach` reducers _before_ parent reducers that can modify the dictionary.
496+
///
345497
/// - Parameters:
346498
/// - toLocalState: A key path that can get/set a dictionary of `State` values inside
347499
/// `GlobalState`.

0 commit comments

Comments
 (0)