How do you reason about nested states and state "ownership"?

Hey,

Just curious to hear other people's thoughts on nested states (and reducers) and how to divide responsibilities.

Say I have an app sort of like the app store, where there's a "dashboard" view that shows different lists of "items". You can either tap "See all" to see a comprehensive view of all the items in that list, or tap an item directly to view the details of the item. Or you can tap an item directly in the top view to go to the item's detail view.

Each view has their own state, store and "default reducer". Perhaps even split up into three modules.

So in this example would you have the "dashboard view" have to optional states:

struct DashboardState: Equatable {
  var listState: ListState?
  var itemState: ItemState?
}

And would ListState in that case also have it's own ItemState? And furthermore, would the reducer for the list view also combine a pulled back version of the item view reducer? And if so, would the dashboard's reducer also include a pulled back version of the item view reducer?

Or, do you tend to have some "coordinating catch-all" state/reducer for the entire "feature"?

Curious to hear your thoughts on this as I'm struggling to find a model that feels right.

I am struggling to follow the example a bit, but if I understood correctly, it is similar to some cases in the app I am currently working on.

The way I have dealt with it so far is to create a shared list state/reducer that’s used across any screens displaying the list, but keep the selection state in each individual screen. The reasoning behind is that the presentation might change in the future (push, modal, etc) and it’d be far more flexible if it isn’t tied to the list domain.

So in this case, and again provided I understood the kind of UI you are working with, the answers to your questions would be: yes, yes, and yes. Unless the list is the exact same between the two, in which case I’d extract it into its own thing.

Let me attempt a clarification.

Given these three

struct DashboardViewState: Equatable {
  // ..
  var categoryListViewState: CategoryListViewState?
  var itemDetailViewState: ItemDetailViewViewState?
}

enum DashboardViewAction: Equatable {
  case didTapViewAll(section: Int)
  case didTapItem(IndexPath)

  case categoryListViewAction(CategoryListViewAction)
  case itemDetailViewAction(ItemDetailViewAction)
}

let dashboardReducer = Reducer<DashboardViewState, DashboardViewAction, Void> { state, action, _ in
  switch action {
  case let .didTapViewAll(section: section):
    state.categoryListViewState = CategoryListViewState(category: state.categoryForSection(section))
    return .none
  case let .didTapItem(indexPath):
    state.itemDetailViewState = ItemDetailViewState(item: state.itemForIndexPath(indexPath))
    return .none
  }
}
struct CategoryListViewState: Equatable {
  // ...
  var itemDetailViewState: ItemDetailViewViewState?
}

enum CategoryListViewAction: Equatable {
  case .didTapItem(Item)
  case itemDetailViewAction(ItemDetailViewAction)
}

let categoryListReducer = Reducer< CategoryListViewState, CategoryListViewAction, Void> { state, action, _ in
  switch action {
  case let .didTapItem(item):
    state.itemDetailViewState: ItemDetailViewState(item: item)
    return .none
  }
}
struct ItemDetailViewViewState: Equatable {
  var item: Item
}

enum ItemDetailViewAction: Equatable {
  case didTapSaveFavorite
}

let itemDetailReducer: Reducer<ItemDetailViewViewState, ItemDetailViewAction, Void> { state, action, _ in
  // ..
}

(Code written inside this text editor and not tested in any way)

Now, to make each of these state's respective view's fully work "individually" I have to pullback the itemDetailReducer and combine it with both the dashboardReducer and the categoryListReducer.

Or the whole thing could be architected in such a way where each individual view does not know or care about navigation. Much like the "Coordinator pattern". Another higher order reducer or perhaps just some "app reducer" would listen for "navigation events" and manage the view controller flow.

So, the alternative would be something like this, with removing each individual "substatate" from the different states:

struct AppState: Equatable {
  var dashboardViewState: DashboardViewState
  var categoryListViewState: CategoryListViewState?
  var itemDetailViewState: ItemDetailViewState?
}

enum AppAction: Equatable {
  case dashboardAction(DashboardViewAction)
  case categoryListAction(CategoryListViewAction)
}

let appReducer = Reducer<AppState, AppAction, Void> { state, action, _ in
  switch action {
    case .dashboardAction(.didTapCategory(let category)):
      state.categoryListViewState = CategoryListViewState(category)
      return .none
    case .dashboardAction(.didTapItem(let item),
         .categoryListAction(.didTapItem(let item)):
     state.itemDetailViewState(item)
     return .none
  }
}

Does that make it clearer?

I guess I just find it a little odd that if one were to visualize the "network" of reducers, in the first case we'd end up with something like:

dashboardReducer
  - listReducer
    - itemReducer
  - itemReducer

Which feels redundant. But maybe that's just the beauty of TCA? Being able to compose stuff together and not having to generalize everything. :slight_smile: