Scaling Multiple States

I am so excited about TCA that i started, as a side project migrating my company's codebase to it.

As I add more features to the app I came to the following problem. Each feature has its own state but parts of that state might be shared with other features, e.g articles appearing both in the home screen and in a dedicated screen.

Further more each feature will probably (surely) require an isLoading flag (for when a request is being performed) and an error: Swift.Error? for when requests fail.

I have experimented with the following:

  1. Have an AppState which contains every piece of data all other states might require:
struct AppState {
    var articles: [Article]
    var toDos: [ToDo]
    var isLoadingArticles: Bool
    var isLoadingToDos: Bool
    var errorInArticles: Error?
    var errorInToDos: Error?
}

extension AppState {
    var articles: ArticleState {
       get { ... }
       set { ... }
    }
}

this will probable get the job done but, imo, will create issues down the line when the features become too many and AppState will be difficult to read and understand. (I also think it adds too much noise).

  1. Instead of polluting AppState with isLoading<FeatureX> and errorIn<FeatureX> I could share error and isLoading with every derived state like so:
struct AppState {
    var articles: [Article]
    var toDos: [ToDo]
    var isLoading: Bool
    var error: Error?
}

extension AppState {
    var articles: ArticleState {
       get { ... }
       set { ... }
    }
}

but then the app risks showing activity indicators and errors in places it shouldn't. Derived states though can still share information.

  1. Isolate derived states and store them directly in AppState. That way there is no pollution on AppState but sharing information becomes imposible :cry:.

Is there an optimal solution to this?

My experience with this architecture is all from Elm but I think the following applies for SCA as well. A common approach for this problem would be to model the states with an enum.

enum RemoteData<Value, Error> {
  case notAsked
  case loading
  case failure(Error)
  case success(Value)
}

Then your model would become

struct AppState {
  var articles: RemoteData<[Article], String> = .notAsked
  var toDos: RemoteData<[ToDo], String> = .notAsked
}

and to use

var appState = AppState()
appState.todos = .success([Todo("Make breakfast")])

struct TodosView: View {
  var todos: RemoteData<Todo, String>

  public var body: some View {
    switch todos {
      case .notAsked: return AnyView(Button(action: {}) { Text("Load todos") }).id("NotAsked")
      case .loading: return AnyView(Text("Loading...")).id("Loading")
      case .failure(let message): return AnyView(Text(message)).id("Failure")
      case .success(let todos): return AnyView(TodosListView(todos)).id("Success")
    }
  }
}

There's a couple of advantages here.

  1. You don't have to repeat all of the state fields for each of todos, articles etc.
  2. The compiler will now force you to deal with all of the states that todos, articles might be in. You'll never forget to show a loader in your view for instance because the compiler will force you to handle that case.

Unfortunately the Swift type erasure makes the view code in the example more complicated than you'd want (AnyView etc). But you could always have a view that wrapped this up so that you could do e.g.

RemoteDataView(todos, 
  notAsked: { Button(action: {}) { Text("Load todos") }) },
  loading: { Text("Loading...") },
  failure: { error in Text(error) },
  success: { todos in TodosListView(todos) } 
)
3 Likes

I think another option is to have AppState contain an articlState property

struct AppState {
  var articleState: ArticleState
}

This articleState could be initialized at appState initialization, or at a later time in a reducer.

Yes, if the article data wasn't shared between substates then I'd also take this approach, let each substate handle it's own data. I'd then use RemoteData inside those substates to track the state of the actual data e.g.

struct AppState {
  var articleState: ArticleState
}

struct ArticleState {
  var articles: RemoteData<[Article], String>
}
Terms of Service

Privacy Policy

Cookie Policy