Emitting global actions" in TCA

I was wondering if there were best practices for "global actions." I've found a few cases where I've more or less wanted to delegate an action for parent or root domains to handle. As a simple example "logout." This action occurrs in a few different views/domains in my app, but the logic is always the same and in my case owned by the root domain. I've found two issues/questions that arose while I've been implementing that I wanted to see if others' had a better suggestion for handling.

  1. In order to scope the child reducers in the root domain, I need to have a reference to the child state as a required parameter to Scope. While I can just initialize the state of each child domain, it seems unnecessary especially when the state object isn't small.
  2. Each child domain that has a logout action needs ot be handled independently at the root domain. It seems like a lot of additional code to handle even if there is shared logic in the root domain.

Am I doing anything wrong/is there a better way to organize this?

I've also been using some of the suggestions from TCA Action Boundaries (hence the delegate pattern)

Here's more or less what my root domain looks like

struct RootDomain: ReducerProtocol {

    struct State: Equatable {
        var settingsState = SettingsDomain.State()
        var lockState = LockDomain.State()
    }
    
    enum Action {
        case lockLogout(LockDomain.Action)
        case settingsLogout(SettingsDomain.Action)
    }
    
    var body: some ReducerProtocol<State, Action> {
        Scope(state: \.settingsState, action: /Action.logout) {
            SettingsDomain()
        }
        Scope(state: \.lockState, action: /Action.logout) {
            LockDomain()
        }
        Reduce { _, action in
            switch action {
            case let .lockLogout(.delegate(action)):
                switch action {
                case .userLoggedOut:
                    // shared logout
                }
            case let .settingsLogout(.delegate(action)):
                switch action {
                case .userLoggedOut:
                    // shared logout
                }
            }
        }
    }
}

I would've preferred something like delegating a global logout action

To scope reducers you provide way to transform parent state to child state and the same with actions. But it's just a function. What you initialize is ReducerProtocol instance if most of the time it's very lightweight with very simple init. State is initialized with Store and managed by it. As I understand everything correctly, then you can transform/compose Reducer as much as you like but state object is created once.

Maybe I don't understand something but to be honest I don't see much problem here. What's wrong with that in your use case? Sure it's a little verbose with explicitly handling every subdomain action in reducer. But that how things are done in TCA, where clear intent is more important than short code. Please notice, that you use additional pattern which put even more focus on explicit intent communication.
If you flatten your nested switches then you can group all logout action in one case:

{...}
case 
  .settingsLogout(.delegate(.userLoggedOut)),
  .lockLogout(.delegate(.userLoggedOut)):
  self.performLogout()  // logout logic moved to separate method 
{...}

Personally I would prefer to move all logout logic to separate leaf feature integrated to all domains which need this. When I need to share some functionality with different features, then most of the time creating separate feature for handling shared functionality is my way to go. However It depends what really is this logout logic and if it really worth to make separate feature for this.

As an fast track alternative approach to communicate events from deep subdomain to the parent you can use separate client registered in Dependencies. Just create logout client with AsyncStream which child domains can use to communicate some events and parent is listening and reacting to this events. It can be really convenient but I still don't know if it's not just a lazy hack ;)

Both approaches are really the same concept when child domains send some events and parent listing to them. But using two different mechanisms available in TCA.

1 Like

I have a similar thing with a dependency like Malaunch mentioned. I don’t think it’s a hack at all. And it does make it more consistent to do logouts. The dependency can do things like clear out stored data etc… and then emit an AsyncStream which lets the view get updated so the app is pushed back to the login screen.

1 Like

I just don't have really solid opinion on this approach yet. I would say that it depends strongly of use case. If it's really some global behaviour, managing side effect, doing async work then I would say that it's just normal "client dependency". However if it's only to make parent reducer action switch statement shorter than I would prefer to avoid creating second, parallel action flow.

Recently I had similar dilemma about computed properties in State structs. Is it ok to use them or this logic should be in reduce method? My conclusion was that in a long term is much better to put as much as possible logic into Reducer even if it's more verbose. It's just easier to comprehend logic in one place and one flow of action when debugging or refactoring and works better with TCA tools for testing.

1 Like