Combining stores to extend the scope

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:

3 Likes

This is a great idea! I would support this being implemented

2 Likes

Yes, I’m interested in the same kind of thing.

In my case it’d be useful to have a separate store for the state of the UI. It does not have to be propagated down to the root store in the same sense as the content offset of a scroll view gets encapsulated in the scroll view and not stored in the database :slightly_smiling_face:

That’d be really useful to have a standard way of doing that!

2 Likes