Reducer - mbrandonw/swift-composable-architecture GitHub Wiki
A reducer describes how to evolve the current state of an application to the next state, given
an action, and describes what Effect
s should be executed later by the store, if any.
public struct Reducer<State, Action, Environment>
Reducers have 3 generics:
Note: The thread on which effects output is important. An effect's output is immediately sent back into the store, and `Store` is not thread safe. This means all effects must receive values on the same thread, **and** if the `Store` is being used to drive UI then all output must be on the main thread. You can use the `Publisher` method `receive(on:)` for make the effect output its values on the thread of your choice.
Initializes a reducer from a simple reducer function signature.
public init(_ reducer: @escaping (inout State, Action, Environment) -> Effect<Action, Never>)
The reducer takes three arguments: state, action and environment. The state is inout
so that
you can make any changes to it directly inline. The reducer must return an effect, which
typically would be constructed by using the dependencies inside the environment
value. If
no effect needs to be executed, a .none
effect can be returned.
For example:
struct MyState { var count = 0, text = "" }
enum MyAction { case buttonTapped, textChanged(String) }
struct MyEnvironment { var analyticsClient: AnalyticsClient }
let myReducer = Reducer<MyState, MyAction, MyEnvironment> { state, action, environment in
switch action {
case .buttonTapped:
state.count += 1
return environment.analyticsClient.track("Button Tapped")
case .textChanged(let text):
state.text = text
return .none
}
}
- reducer: - reducer: A function signature that takes state, action and environment.
var optional: Reducer<State?, Action, Environment>
let reducer: (inout State, Action, Environment) -> Effect<Action, Never>
A reducer that performs no state mutations and returns no effects.
var empty: Reducer
Prints debug messages describing all received actions and state mutations.
public func debug(_ prefix: String = "", actionFormat: ActionFormat = .prettyPrint, environment toDebugEnvironment: @escaping (Environment) -> DebugEnvironment = { _ in
DebugEnvironment()
}) -> Reducer
Printing is only done in debug (#if DEBUG
) builds.
- prefix: - prefix: A string with which to prefix all debug messages.
- toDebugEnvironment: - toDebugEnvironment: A function that transforms an environment into a debug environment by describing a print function and a queue to print from. Defaults to a function that ignores the environment and returns a default
DebugEnvironment
that uses Swift'sprint
function and a background queue.
A reducer that prints debug messages for all received actions.
Prints debug messages describing all received actions.
public func debugActions(_ prefix: String = "", actionFormat: ActionFormat = .prettyPrint, environment toDebugEnvironment: @escaping (Environment) -> DebugEnvironment = { _ in
DebugEnvironment()
}) -> Reducer
Printing is only done in debug (#if DEBUG
) builds.
- prefix: - prefix: A string with which to prefix all debug messages.
- toDebugEnvironment: - toDebugEnvironment: A function that transforms an environment into a debug environment by describing a print function and a queue to print from. Defaults to a function that ignores the environment and returns a default
DebugEnvironment
that uses Swift'sprint
function and a background queue.
A reducer that prints debug messages for all received actions.
Prints debug messages describing all received local actions and local state mutations.
public func debug<LocalState, LocalAction>(_ prefix: String = "", state toLocalState: @escaping (State) -> LocalState, action toLocalAction: CasePath<Action, LocalAction>, actionFormat: ActionFormat = .prettyPrint, environment toDebugEnvironment: @escaping (Environment) -> DebugEnvironment = { _ in
DebugEnvironment()
}) -> Reducer
Printing is only done in debug (#if DEBUG
) builds.
- prefix: - prefix: A string with which to prefix all debug messages.
- toLocalState: - toLocalState: A function that filters state to be printed.
- toLocalAction: - toLocalAction: A case path that filters actions that are printed.
- toDebugEnvironment: - toDebugEnvironment: A function that transforms an environment into a debug environment by describing a print function and a queue to print from. Defaults to a function that ignores the environment and returns a default
DebugEnvironment
that uses Swift'sprint
function and a background queue.
A reducer that prints debug messages for all received actions.
Instruments the reducer with signposts. Each invocation of the reducer will be measured by an interval, and the lifecycle of its effects will be measured with interval and event signposts.
public func signpost(_ prefix: String = "", log: OSLog = OSLog(
subsystem: "co.pointfree.composable-architecture",
category: "Reducer Instrumentation"
)) -> Self
To use, build your app for Instruments (⌘I), create a blank instrument, and then use the "+" icon at top right to add the signpost instrument. Start recording your app (red button at top left) and then you should see timing information for every action sent to the store and every effect executed.
Effect instrumentation can be particularly useful for inspecting the lifecycle of long-living
effects. For example, if you start an effect (e.g. a location manager) in onAppear
and
forget to tear down the effect in onDisappear
, it will clearly show in Instruments that the
effect never completed.
- prefix: - prefix: A string to print at the beginning of the formatted message for the signpost.
- log: - log: An
OSLog
to use for signposts.
A reducer that has been enhanced with instrumentation.
@available(*, unavailable, renamed: "debug(_:environment:)") public func debug(prefix: String, environment toDebugEnvironment: @escaping (Environment) -> DebugEnvironment = { _ in
DebugEnvironment()
}) -> Reducer
@available(*, unavailable, renamed: "debugActions(_:environment:)") public func debugActions(prefix: String, environment toDebugEnvironment: @escaping (Environment) -> DebugEnvironment = { _ in
DebugEnvironment()
}) -> Reducer
@available(*, unavailable, renamed: "debug(_:state:action:environment:)") public func debug<LocalState, LocalAction>(prefix: String, state toLocalState: @escaping (State) -> LocalState, action toLocalAction: CasePath<Action, LocalAction>, environment toDebugEnvironment: @escaping (Environment) -> DebugEnvironment = { _ in
DebugEnvironment()
}) -> Reducer
Combines many reducers into a single one by running each one on state in order, and merging all of the effects.
public static func combine(_ reducers: Reducer) -> Reducer
It is important to note that the order of combining reducers matter. Combining reducerA
with
reducerB
is not necessarily the same as combining reducerB
with reducerA
.
This can become an issue when working with reducers that have overlapping domains. For
example, if reducerA
embeds the domain of reducerB
and reacts to its actions or modifies
its state, it can make a difference if reducerA
chooses to modify reducerB
's state
before or after reducerB
runs.
This is perhaps most easily seen when working with optional
reducers, where the parent
domain may listen to the child domain and nil
out its state. If the parent reducer runs
before the child reducer, then the child reducer will not be able to react to its own action.
Similar can be said for a forEach
reducer. If the parent domain modifies the child
collection by moving, removing, or modifying an element before the forEach
reducer runs, the
forEach
reducer may perform its action against the wrong element, an element that no longer
exists, or an element in an unexpected state.
Running a parent reducer before a child reducer can be considered an application logic error, and can produce assertion failures. So you should almost always combine reducers in order from child to parent domain.
Here is an example of how you should combine an optional
reducer with a parent domain:
let parentReducer = Reducer<ParentState, ParentAction, ParentEnvironment>.combine(
// Combined before parent so that it can react to `.dismiss` while state is non-`nil`.
childReducer.optional.pullback(
state: \.child,
action: /ParentAction.child,
environment: { $0.child }
),
// Combined after child so that it can `nil` out child state upon `.child(.dismiss)`.
Reducer { state, action, environment in
switch action
case .child(.dismiss):
state.child = nil
return .none
...
}
},
)
- reducers: - reducers: A list of reducers.
A single reducer.
Combines many reducers into a single one by running each one on state in order, and merging all of the effects.
public static func combine(_ reducers: [Reducer]) -> Reducer
It is important to note that the order of combining reducers matter. Combining reducerA
with
reducerB
is not necessarily the same as combining reducerB
with reducerA
.
This can become an issue when working with reducers that have overlapping domains. For
example, if reducerA
embeds the domain of reducerB
and reacts to its actions or modifies
its state, it can make a difference if reducerA
chooses to modify reducerB
's state
before or after reducerB
runs.
This is perhaps most easily seen when working with optional
reducers, where the parent
domain may listen to the child domain and nil
out its state. If the parent reducer runs
before the child reducer, then the child reducer will not be able to react to its own action.
Similar can be said for a forEach
reducer. If the parent domain modifies the child
collection by moving, removing, or modifying an element before the forEach
reducer runs, the
forEach
reducer may perform its action against the wrong element, an element that no longer
exists, or an element in an unexpected state.
Running a parent reducer before a child reducer can be considered an application logic error, and can produce assertion failures. So you should almost always combine reducers in order from child to parent domain.
Here is an example of how you should combine an optional
reducer with a parent domain:
let parentReducer = Reducer<ParentState, ParentAction, ParentEnvironment>.combine(
// Combined before parent so that it can react to `.dismiss` while state is non-`nil`.
childReducer.optional.pullback(
state: \.child,
action: /ParentAction.child,
environment: { $0.child }
),
// Combined after child so that it can `nil` out child state upon `.child(.dismiss)`.
Reducer { state, action, environment in
switch action
case .child(.dismiss):
state.child = nil
return .none
...
}
},
)
- reducers: - reducers: An array of reducers.
A single reducer.
Combines many reducers into a single one by running each one on state in order, and merging all of the effects.
public func combined(with other: Reducer) -> Reducer
It is important to note that the order of combining reducers matter. Combining reducerA
with
reducerB
is not necessarily the same as combining reducerB
with reducerA
.
This can become an issue when working with reducers that have overlapping domains. For
example, if reducerA
embeds the domain of reducerB
and reacts to its actions or modifies
its state, it can make a difference if reducerA
chooses to modify reducerB
's state
before or after reducerB
runs.
This is perhaps most easily seen when working with optional
reducers, where the parent
domain may listen to the child domain and nil
out its state. If the parent reducer runs
before the child reducer, then the child reducer will not be able to react to its own action.
Similar can be said for a forEach
reducer. If the parent domain modifies the child
collection by moving, removing, or modifying an element before the forEach
reducer runs, the
forEach
reducer may perform its action against the wrong element, an element that no longer
exists, or an element in an unexpected state.
Running a parent reducer before a child reducer can be considered an application logic error, and can produce assertion failures. So you should almost always combine reducers in order from child to parent domain.
Here is an example of how you should combine an optional
reducer with a parent domain:
let parentReducer: Reducer<ParentState, ParentAction, ParentEnvironment> =
// Run before parent so that it can react to `.dismiss` while state is non-`nil`.
childReducer
.optional
.pullback(
state: \.child,
action: /ParentAction.child,
environment: { $0.child }
)
// Combined after child so that it can `nil` out child state upon `.child(.dismiss)`.
.combined(
with: Reducer { state, action, environment in
switch action
case .child(.dismiss):
state.child = nil
return .none
...
}
}
)
- other: - other: Another reducer.
A single reducer.
Transforms a reducer that works on local state, action and environment into one that works on global state, action and environment. It accomplishes this by providing 3 transformations to the method:
public func pullback<GlobalState, GlobalAction, GlobalEnvironment>(state toLocalState: WritableKeyPath<GlobalState, State>, action toLocalAction: CasePath<GlobalAction, Action>, environment toLocalEnvironment: @escaping (GlobalEnvironment) -> Environment) -> Reducer<GlobalState, GlobalAction, GlobalEnvironment>
This operation is important for breaking down large reducers into small ones. When used with
the combine
operator you can define many reducers that work on small pieces of domain, and
then pull them back and combine them into one big reducer that works on a large domain.
// Global domain that holds a local domain:
struct AppState { var settings: SettingsState, /* rest of state */ }
struct AppAction { case settings(SettingsAction), /* other actions */ }
struct AppEnvironment { var settings: SettingsEnvironment, /* rest of dependencies */ }
// A reducer that works on the local domain:
let settingsReducer = Reducer<SettingsState, SettingsAction, SettingsEnvironment> { ... }
// Pullback the settings reducer so that it works on all of the app domain:
let appReducer: Reducer<AppState, AppAction, AppEnvironment> = .combine(
settingsReducer.pullback(
state: \.settings,
action: /AppAction.settings,
environment: { $0.settings }
),
/* other reducers */
)
- toLocalState: - toLocalState: A key path that can get/set
State
insideGlobalState
. - toLocalAction: - toLocalAction: A case path that can extract/embed
Action
fromGlobalAction
. - toLocalEnvironment: - toLocalEnvironment: A function that transforms
GlobalEnvironment
intoEnvironment
.
A reducer that works on GlobalState
, GlobalAction
, GlobalEnvironment
.
Transforms a reducer that works on non-optional state into one that works on optional state by only running the non-optional reducer when state is non-nil.
public func optional(_ file: StaticString = #file, _ line: UInt = #line) -> Reducer<
State?, Action, Environment
>
Often used in tandem with pullback
to transform a reducer on a non-optional local domain
into a reducer that can be combined with a reducer on a global domain that contains some
optional local domain:
// Global domain that holds an optional local domain:
struct AppState { var modal: ModalState? }
struct AppAction { case modal(ModalAction) }
struct AppEnvironment { var mainQueue: AnySchedulerOf<DispatchQueue> }
// A reducer that works on the non-optional local domain:
let modalReducer = Reducer<ModalState, ModalAction, ModalEnvironment { ... }
// Pullback the local modal reducer so that it works on all of the app domain:
let appReducer = Reducer<AppState, AppAction, AppEnvironment>.combine(
modalReducer.optional().pullback(
state: \.modal,
action: /AppAction.modal,
environment: { ModalEnvironment(mainQueue: $0.mainQueue) }
),
Reducer { state, action, environment in
...
}
)
Take care when combining optional reducers into parent domains, as order matters. Always
combine optional reducers before parent reducers that can nil
out the associated optional
state.
A version of pullback
that transforms a reducer that works on an element into one that works
on a collection of elements.
public func forEach<GlobalState, GlobalAction, GlobalEnvironment>(state toLocalState: WritableKeyPath<GlobalState, [State]>, action toLocalAction: CasePath<GlobalAction, (Int, Action)>, environment toLocalEnvironment: @escaping (GlobalEnvironment) -> Environment, _ file: StaticString = #file, _ line: UInt = #line) -> Reducer<GlobalState, GlobalAction, GlobalEnvironment>
// Global domain that holds a collection of local domains:
struct AppState { var todos: [Todo] }
struct AppAction { case todo(index: Int, action: TodoAction) }
struct AppEnvironment { var mainQueue: AnySchedulerOf<DispatchQueue> }
// A reducer that works on a local domain:
let todoReducer = Reducer<Todo, TodoAction, TodoEnvironment> { ... }
// Pullback the local todo reducer so that it works on all of the app domain:
let appReducer = Reducer<AppState, AppAction, AppEnvironment>.combine(
todoReducer.forEach(
state: \.todos,
action: /AppAction.todo(index:action:),
environment: { _ in TodoEnvironment() }
),
Reducer { state, action, environment in
...
}
)
Take care when combining forEach
reducers into parent domains, as order matters. Always
combine forEach
reducers before parent reducers that can modify the collection.
- toLocalState: - toLocalState: A key path that can get/set an array of
State
elements inside.GlobalState
. - toLocalAction: - toLocalAction: A case path that can extract/embed
(Int, Action)
fromGlobalAction
. - toLocalEnvironment: - toLocalEnvironment: A function that transforms
GlobalEnvironment
intoEnvironment
.
A reducer that works on GlobalState
, GlobalAction
, GlobalEnvironment
.
A version of pullback
that transforms a reducer that works on an element into one that works
on an identified array of elements.
public func forEach<GlobalState, GlobalAction, GlobalEnvironment, ID>(state toLocalState: WritableKeyPath<GlobalState, IdentifiedArray<ID, State>>, action toLocalAction: CasePath<GlobalAction, (ID, Action)>, environment toLocalEnvironment: @escaping (GlobalEnvironment) -> Environment, _ file: StaticString = #file, _ line: UInt = #line) -> Reducer<GlobalState, GlobalAction, GlobalEnvironment>
// Global domain that holds a collection of local domains:
struct AppState { var todos: IdentifiedArrayOf<Todo> }
struct AppAction { case todo(id: Todo.ID, action: TodoAction) }
struct AppEnvironment { var mainQueue: AnySchedulerOf<DispatchQueue> }
// A reducer that works on a local domain:
let todoReducer = Reducer<Todo, TodoAction, TodoEnvironment> { ... }
// Pullback the local todo reducer so that it works on all of the app domain:
let appReducer = Reducer<AppState, AppAction, AppEnvironment>.combine(
todoReducer.forEach(
state: \.todos,
action: /AppAction.todo(id:action:),
environment: { _ in TodoEnvironment() }
),
Reducer { state, action, environment in
...
}
)
Take care when combining forEach
reducers into parent domains, as order matters. Always
combine forEach
reducers before parent reducers that can modify the collection.
- toLocalState: - toLocalState: A key path that can get/set a collection of
State
elements insideGlobalState
. - toLocalAction: - toLocalAction: A case path that can extract/embed
(Collection.Index, Action)
fromGlobalAction
. - toLocalEnvironment: - toLocalEnvironment: A function that transforms
GlobalEnvironment
intoEnvironment
.
A reducer that works on GlobalState
, GlobalAction
, GlobalEnvironment
.
A version of pullback
that transforms a reducer that works on an element into one that works
on a dictionary of element values.
public func forEach<GlobalState, GlobalAction, GlobalEnvironment, Key>(state toLocalState: WritableKeyPath<GlobalState, [Key: State]>, action toLocalAction: CasePath<GlobalAction, (Key, Action)>, environment toLocalEnvironment: @escaping (GlobalEnvironment) -> Environment, _ file: StaticString = #file, _ line: UInt = #line) -> Reducer<GlobalState, GlobalAction, GlobalEnvironment>
Take care when combining forEach
reducers into parent domains, as order matters. Always
combine forEach
reducers before parent reducers that can modify the dictionary.
- toLocalState: - toLocalState: A key path that can get/set a dictionary of
State
values insideGlobalState
. - toLocalAction: - toLocalAction: A case path that can extract/embed
(Key, Action)
fromGlobalAction
. - toLocalEnvironment: - toLocalEnvironment: A function that transforms
GlobalEnvironment
intoEnvironment
.
A reducer that works on GlobalState
, GlobalAction
, GlobalEnvironment
.
Runs the reducer.
public func run(_ state: inout State, _ action: Action, _ environment: Environment) -> Effect<Action, Never>
- state: - state: Mutable state.
- action: - action: An action.
- environment: - environment: An environment.
An effect that can emit zero or more actions.
public func callAsFunction(_ state: inout State, _ action: Action, _ environment: Environment) -> Effect<Action, Never>