Hi there!
I recently ran into a situation in which I wanted a part of the app state to only 'live' in a certain part of the app (a flow that completes and then adds its result to the AppState) and not actually be part of the AppState.
We can see Stores as views / scopes into our App domain. Whenever we call .scope on a certain store, we basically get a smaller/mapped scope of the app domain. A state therefore is a slice of the app domain. This means, that all values that make up the state of the application need to be reflected in the AppState struct as they are all part of the same domain.
I would like to propose adding the opposite of scoping
, which I would describe as combining stores.
Let's say we have a store of AppState
and a store of LocalState
. AppState
and LocalState
are mutually exclusive and encapsulate two different domains: The local domain (for example for a certain flow) and the global domain (i.e. the app domain)
let appStateStore = Store<AppState, AppAction>(
initialState: AppState(),
reducer: appReducer,
environment: AppEnvironment(...)
)
let localStateStore = Store<LocalState, LocalAction>(
initialState: LocalState(),
reducer: localReducer,
environment: LocalEnvironment()
)
I would propose that we allow combining these two stores into a Store<(AppState, LocalState), Either<AppAction, LocalAction>>
. The resulting store combines the app domain with the local domain and allows to perform actions on both. As the domains are mutually exclusive, changes in one of the domains shouldn't effect the other one. The combined store can be mapped into a view-specific Store<ViewState, ViewAction>
.
We could introduce a scoping operator combine
similar to the existing scope
operator.
enum Either<A, B> {
case a(A)
case b(B)
}
func combine<OtherState, OtherAction>(_ other: Store<OtherState, OtherAction>) -> Store<(State, OtherState)>, Either<Action, OtherAction>> {
let localStore = Store(
initialState: (self.state.value, other.state.value),
reducer: { localState, action in
switch action {
case let .a(action):
self.send(action)
case let .b(action):
other.send(action)
}
localState = (self.state.value, other.state.value)
return .none
}
)
localStore.parentCancellable = self.state
.combineLatest(other.state)
.sink { [weak localStore] (a, b) in
localStore?.state.value = (a, b)
}
return localStore
}
This is just a rough idea of how it could be implemented. As Swift does not support variadic generics yet, we would run into a problem that even the Combine framework ran into: supporting combining more than two stores would require to rewrite this function. However, this code could be autogenerated using .gyb
or other code generation tools.
Looking forward to your feedback!
EDIT: Here's a Venn diagram to back up the idea: