Private actions and state

Hi everyone!

In the company I work, we use a Redux/ELM inspired architecture since 3/4 years now. However, we are hitting some modularization issues and we are starting to explore TCA as the potential next architecture to use. I watched all the videos, but there is one thing I cannot figure out and I'm not sure whether it is a limitation of TCA, something I don't understand how to do or even a wrong approach to the problem.

We have many applications, and we share reusable libraries across our apps. Each library has its own state (with private and public information) and functionalities.

One thing we really like is to have a single state for all the information (both application ones and libraries ons) so we can store it and, with the proper abstraction, information can be shared even across libraries.

The problem I don't know how to solve is how to make so that libraries can update their internal state (through an action + reducer) but avoid that the app can mistakenly use the action that updates the internal state.

To make an example (obliviously super simplified), let's say we have the following state in the library

public struct LibState {
 // the products fetched using StoreKit
 // (note: in a real world example, we would not persist SKProduct instances, but
 // rather we would use our own struct
 public var products: [SKProduct]

 // when the lib "starts" the products are fetched automatically.
 // this bool keeps track of whether the fetching flow is ongoing
 internal var isFetchingProducts: Bool
}

the lib may also have an action's enum like the following

public enum LibAction {
  case toggleIsFetching
  case didReceiveProducts([SKProduct])
}

and finally a reducer that changes the state and that will be composed in the app's reducer

// library
enum Lib {
 static let reducer = Reducer<LibState, LibAction, LibEnvironment> { ... }
}


// app
let appReducer = Reducer<AppState, AppAction, AppEnvironment> { ... }
  .combine(
   Lib.reducer.pullback(\.libState, /AppAction.lib, LibEnvironment())
  )

The problem is that the app can both see and use both LibAction.toggleIsFetching and LibAction.didReceiveProducts. While being able to reduce over LibAction.didReceiveProducts could be a good thing (e.g., you could do something when products are updated), LibAction.toggleIsFetching is a problem because it should be an internal detail of the lib. Also, in both cases the app should not be able to send those actions.

We could also make so that we don't use the TCA mechanism at all (or we use it with a separated, internal store, hidden from the app) but I really like having a single, coherent store where information is stored and shared (also to avoid issues that some web libraries, such as Flux, had in the past).

You can have something along those lines:

public enum {
    case publicAction1
    case publicAction2
    case internalAction(InternalAction)
}

public struct InternalAction {
    enum InternalAction {
        case privateAction1
        case privateAction2
    }

    let action: InternalAction
}

You can do the same with state:

public struct State {
    struct InternalState {}
    public var property1, property2
    var internalState = InternalState()
}

If you put this in a module you can't see internal state, you can see that there are internal actions but you can't send / handle them

Hi, thanks for the reply.
I'm playing with something similar but using a case path to completely separate the "internal" version from the "public" version of the actions. This is because I don't like that the entire shape of the enum action should be defined around the separation public/internal rather than around features

Hi there !

I'm facing the same problem here. Setting the top action enum public implies making the whole hierarchy of actions public because one of the cases wraps the child reducer actions.

@bolismauro have you managed to solve it using case paths ?

Thanks in advance :)