Skip to content

Commit bf5302c

Browse files
stephencelismluisbrown
authored andcommitted
Document scope a bit more deeply (#360)
1 parent 43df2ad commit bf5302c

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

453455
/// Scopes a store to assert against more local state.
454456
///
457+
/// Useful for testing view store-specific state.
458+
///
455459
/// - Parameter toLocalState: A function that transforms the reducer's state into more local
456460
/// state. This state will be asserted against as it is mutated by the reducer. Useful for
457461
/// testing view store state transformations.

0 commit comments

Comments
 (0)