Distinction between persistent state (e.g. domain models) and ephemeral state (e.g. UI state)

I've really enjoyed watching (then rewatching portions of) the video series as I put it into practice in a sample app of my own. I really like the idea of SCA (or is it TCA?) and I think I finally grok things well enough to start asking questions.

One struggle I have is that the idea of maintaining all the UI state and actions at the root level feels wrong. Wouldn't there be an occasion to hide temporary state or local actions so that they don't leak from the lower level to the highest level?

My initial thoughts (in order of likelihood to me):

  1. I am missing something fundamental
  2. The examples are overly simplified for the same of demonstration
  3. The intent is just that: references to everything will live at the root

I would appreciate any pointers to blog articles, sections of existing videos or articles, or anything that helps me understand better.

Thanks!
Doug

5 Likes

Hi @dwsjoquist! How do you think, maybe it's that you are looking for?

  struct ViewState: Equatable {
    var alertData: AlertData?
    var code: String
    var isActivityIndicatorVisible: Bool
    var isFormDisabled: Bool
    var isSubmitButtonDisabled: Bool
  }


  enum ViewAction {
    case alertDismissed
    case codeChanged(String)
    case submitButtonTapped
  }


  let store: Store<TwoFactorState, TwoFactorAction>


  public init(store: Store<TwoFactorState, TwoFactorAction>) {
    self.store = store
  }


  public var body: some View {
    WithViewStore(self.store.scope(state: { $0.view }, action: TwoFactorAction.view)) { viewStore in

swift-composable-architecture/TwoFactorSwiftView.swift at 2ce84cce7995b0cc1f8543e26676e29af80e084b · pointfreeco/swift-composable-architecture · GitHub

There are also videos around this topic. Here some of them:

Actually, that illustrates my question. This pulls back the ViewAction to TwoFactorAction here:

WithViewStore(self.store.scope(state: { $0.view }, action: TwoFactorAction.view)) { viewStore in

Which requires every view action to be mapped to a higher level action, all the way back to the root level (AppAction/AppState), right?

Yes and no, I guess. Usually you separate your app to several modules, you could do it even with a small pieces as it was shown in todo app example. And you map your view actions to the actions of this tiny module, not to the AppEvent.

With ViewState it’s even easier ViewState it’s mapped version of business logic State for easier representation on the view module. Sometimes I put some constant data in ViewState as well, such as scene title or colors

Of course, you could erase some actions/states/views by referring to Any instances or Common types.

For example, if you have following:

enum SelectionAction {
  struct Payload {
    var count: Int { self.ids.count }
    var ids: [String] = .init()
  }
  case didSelect(Payload)
}

You could easily map it to another payload or even plain array with selected ids.

Consider the following:

enum CustomModule {
  typealias Action = String
}

enum AnotherCustomModule {
  typealias Action = String
}

To communicate between modules, you have to write a parser from one action to another. But, essentially, these modules are separated.

In each module you have to write a basic view model or view state or whatever which could handle input and output of events/actions.

And on AppLevel (as someone said) you could keep every module.

enum CustomModule {
  // define MainViewModel which will keep state.
  class Assembly {
    var mainViewModel: MainViewModel
    // subscribe on output publisher to receive events from views of this module.
    var outputPublisher: AnyPublisher<ViewAction, Never> { self.mainViewModel.outputActionPublisher }
    // send input events into this stream
    var inputSubject: PassthroughSubject<InputAction, Never> { self.mainViewModel.inputSubject }
  }
}

@dwsjoquist the SCA is based around the idea that you represent your view state as a state machine with the reducer specifying transitions between valid states.

Which requires every view action to be mapped to a higher level action, all the way back to the root level (AppAction/AppState), right?

I'm having a little trouble understanding what your concern is exactly, is it simply that there's some boilerplate involved in mapping the state/actions up to the root level or is it something to do with controlling access to the information?

No serious concerns, just trying to understand the flavor of the architecture.

The last few years I've been working on several very large scale applications with multiple modules -- each of which manages its own state/actions. The very top level of the app is effectively a module integration layer that creates containers for the other modules to live in.

Each module's internals are private (at least the newer ones) and provide observables to other modules as needed as part of their API/contract for the outside world.

Mostly I'm trying to draw correlations between that style and SCA and how the principles and style of SCA might play out in that kind of large scale app.

@dwsjoquist - I think something similar is on my and @ohitsdaniel 's mind in Combining stores to extend the scope

Furthermore I strongly believe that we shouldn’t store unnecessary states in a reference type store. It is against SwiftUI framework. Such as keeping String bindings for TextField in store class. If I fire search action after completing typing then I shouldn’t keep its state in a reference type but also not keep its state in a property that is all accessible inside view, architecture should provide a proper API similar to current syntax just without storing it.