Skip to content

Commit 636f3fa

Browse files
authored
Document scope a bit more deeply (#360)
1 parent b075960 commit 636f3fa

File tree

2 files changed

+110
-2
lines changed

2 files changed

+110
-2
lines changed

Sources/ComposableArchitecture/Store.swift

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,120 @@ public final class Store<State, Action> {
4242
/// struct AppAction { case login(LoginAction), ... }
4343
///
4444
/// // A store that runs the entire application.
45-
/// let store = Store(initialState: AppState(), reducer: appReducer, environment: ())
45+
/// let store = Store(
46+
/// initialState: AppState(),
47+
/// reducer: appReducer,
48+
/// environment: AppEnvironment()
49+
/// )
4650
///
4751
/// // Construct a login view by scoping the store to one that works with only login domain.
48-
/// let loginView = LoginView(
52+
/// LoginView(
4953
/// store: store.scope(
5054
/// state: { $0.login },
5155
/// action: { AppAction.login($0) }
5256
/// )
5357
/// )
5458
///
59+
/// Scoping in this fashion allows you to better modularize your application. In this case,
60+
/// `LoginView` could be extracted to a module that has no access to `AppState` or `AppAction`.
61+
///
62+
/// Scoping also gives a view the opportunity to focus on just the state and actions it cares
63+
/// about, even if its feature domain is larger.
64+
///
65+
/// For example, the above login domain could model a two screen login flow: a login form followed
66+
/// by a two-factor authentication screen. The second screen's domain might be nested in the
67+
/// first:
68+
///
69+
/// struct LoginState: Equatable {
70+
/// var email = ""
71+
/// var password = ""
72+
/// var twoFactorAuth: TwoFactorAuthState?
73+
/// }
74+
///
75+
/// enum LoginAction: Equatable {
76+
/// case emailChanged(String)
77+
/// case loginButtonTapped
78+
/// case loginResponse(Result<TwoFactorAuthState, LoginError>)
79+
/// case passwordChanged(String)
80+
/// case twoFactorAuth(TwoFactorAuthAction)
81+
/// }
82+
///
83+
/// The login view holds onto a store of this domain:
84+
///
85+
/// struct LoginView: View {
86+
/// let store: Store<LoginState, LoginAction>
87+
///
88+
/// var body: some View { ... }
89+
/// }
90+
///
91+
/// If its body were to use a view store of the same domain, this would introduce a number of
92+
/// problems:
93+
///
94+
/// * The login view would be able to read from `twoFactorAuth` state. This state is only intended
95+
/// to be read from the two-factor auth screen.
96+
///
97+
/// * Even worse, changes to `twoFactorAuth` state would now cause SwiftUI to recompute
98+
/// `LoginView`'s body unnecessarily.
99+
///
100+
/// * The login view would be able to send `twoFactorAuth` actions. These actions are only
101+
/// intended to be sent from the two-factor auth screen (and reducer).
102+
///
103+
/// * The login view would be able to send non user-facing login actions, like `loginResponse`.
104+
/// These actions are only intended to be used in the login reducer to feed the results of
105+
/// effects back into the store.
106+
///
107+
/// To avoid these issues, one can introduce a view-specific domain that slices off the subset of
108+
/// state and actions that a view cares about:
109+
///
110+
/// extension LoginView {
111+
/// struct State: Equatable {
112+
/// var email: String
113+
/// var password: String
114+
/// }
115+
///
116+
/// enum Action: Equatable {
117+
/// case emailChanged(String)
118+
/// case loginButtonTapped
119+
/// case passwordChanged(String)
120+
/// }
121+
/// }
122+
///
123+
/// One can also introduce a couple helpers that transform feature state into view state and
124+
/// transform view actions into feature actions.
125+
///
126+
/// extension LoginState {
127+
/// var view: LoginView.State {
128+
/// .init(email: self.email, password: self.password)
129+
/// }
130+
/// }
131+
///
132+
/// extension LoginView.Action {
133+
/// var feature: LoginAction {
134+
/// switch self {
135+
/// case let .emailChanged(email)
136+
/// return .emailChanged(email)
137+
/// case .loginButtonTapped:
138+
/// return .loginButtonTapped
139+
/// case let .passwordChanged(password)
140+
/// return .passwordChanged(password)
141+
/// }
142+
/// }
143+
/// }
144+
///
145+
/// With these helpers defined, `LoginView` can now scope its store's feature domain into its view
146+
/// domain:
147+
///
148+
/// var body: some View {
149+
/// WithViewStore(
150+
/// self.store.scope(state: { $0.view }, action: { $0.feature })
151+
/// ) { viewStore in
152+
/// ...
153+
/// }
154+
/// }
155+
///
156+
/// This view store is now incapable of reading any state but view state (and will not recompute
157+
/// when non-view state changes), and is incapable of sending any actions but view actions.
158+
///
55159
/// - Parameters:
56160
/// - toLocalState: A function that transforms `State` into `LocalState`.
57161
/// - fromLocalAction: A function that transforms `LocalAction` into `Action`.

Sources/ComposableArchitecture/TestSupport/TestStore.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,8 @@
432432
extension TestStore {
433433
/// Scopes a store to assert against more local state and actions.
434434
///
435+
/// Useful for testing view store-specific state and actions.
436+
///
435437
/// - Parameters:
436438
/// - toLocalState: A function that transforms the reducer's state into more local state. This
437439
/// state will be asserted against as it is mutated by the reducer. Useful for testing view
@@ -454,6 +456,8 @@
454456

455457
/// Scopes a store to assert against more local state.
456458
///
459+
/// Useful for testing view store-specific state.
460+
///
457461
/// - Parameter toLocalState: A function that transforms the reducer's state into more local
458462
/// state. This state will be asserted against as it is mutated by the reducer. Useful for
459463
/// testing view store state transformations.

0 commit comments

Comments
 (0)