Adding new User state and accessing it from "lower stores"

I am building a new app, comparable with Instagram in that you can "explore" a list of photos, then view an individual photo and its comments.

I've structured the state like this, with a top-level AppState container which holds ExploreState, which in turn contains an IdentifiedArrayOf of photos (among other things like filters, a loading property, etc). Each photo can contain a list of comments.

// ---------
// App State
// ---------

struct AppState: Equatable {
  var exploreState = ExploreState()
}

enum AppAction: Equatable {
  case exploreAction(ExploreAction)
}

let appReducer = Reducer<AppState, AppAction, AppEnvironment>.combine(
  Reducer { state, action, _ in
    switch action {
      case .exploreAction:
        return .none
    }
  },
  exploreReducer.pullback(
    state: \.exploreState,
    action: /AppAction.exploreAction,
    environment: { $0 }
  )
)

// -------------
// Explore State
// -------------

struct ExploreState: Equatable {
  var photos: IdentifiedArrayOf<Photo> = .init()
  var loading = false
  var keyword: String?
  var username: String?
  var page = 1
}

enum ExploreAction: Equatable {
  case loadPhotos
  case loadPhotosResponse(Result<[Photo], ApiError>)
  case photo(id: Int, action: PhotoAction)
}

let exploreReducer = Reducer<ExploreState, ExploreAction, AppEnvironment>.combine(
  Reducer { state, action, environment in
    switch action {
      case .loadPhotos:
        // kick off API effect

      case .loadPhotosResponse(.success(let photos)):
				state.photos = photos
        return .none

      case .photo:
        return .none
    }
  },
  photoReducer.forEach(
    state: \.photos,
    action: /ExploreAction.photo(id:action:),
    environment: { $0 }
  )
)

// -----------
// Photo State
// -----------

struct Photo: Codable, Identifiable, Hashable {
  let id: Int
  let user: User
  var image: String
  let thumbnail: String
  var description: String?
  var comments: [Comment]?
}

enum PhotoAction: Equatable {
  case loadComments
  case loadCommentsResponse(Result<[Comment], ApiError>)
}

let photoReducer = Reducer<Photo, PhotoAction, AppEnvironment> { state, action, environment in
  switch action {
    case .loadComments:
      // kick off API effect

    case .loadCommentsResponse(.success(let comments)):
      state.comments = comments
      return .none
  }
}

It all works fine so far, I can create scoped stores and hand them off to coordinators / view models. Loading comments for a photo in the photoStore does update all the way to the top level appStore, everything is great.

let exploreStore = appStore.scope(state: { $0.exploreState }, action: { AppAction.exploreAction($0) })

let photoStore = exploreStore.scope(state: { $0.photos[id: id] }, action: { .photo(id: id, action: $0) })

Okay, so now finally my question :)

Users need to be able to login, once logged in they can upload photos, add their own comments to photos, etc. My thinking was to add some kind of AccountState to the top level AppState, with its own actions and reducer, which would be responsible for all account-related things (login, logout, change profile details, etc). But how would the photo details page, which just gets handed a ViewStore<Photo, PhotoAction> to work with, know if a user is logged in? It seems silly to pass two stores to a viewmodel, right? Like I'd hand it both a ViewStore<Photo, PhotoAction> and a ViewStore<AccountState, AccountAction>?

I am not sure how best to architect this state, any insights would be greatly appreciated, thanks!

3 Likes

I have basically the same question, I just signed up just to comment this :)

In my first attempt of integrating a TCA feature into an existing app (which has ReSwift, RxSwift, Combine, and other approaches mixed wildly), I resorted to having Combine subjects which are subscribed to, to pass state from (originally) ReSwift to the ViewStore. This is quite Frankenstein-y. Obviously, the ReSwift store could be replaced easily with TCA, and it could exist next to the feature stores, but AFAIK the idea of TCA is to have one single store for the whole app.

Here‘s a related post (with no relevant answers): Sharing global state with the array of child states - #3 by damirstuhec

I ended up using multiple stores in some screens which is really not ideal but the only sure way I could make it work by myself. I’d love to refactor it once maybe @stephencelis can add his input but it works for now :man_shrugging:

3 Likes

Here’s an unverified idea I just had:

  • Global state could be shared with CurrentValueSubjects
  • Those subjects are stored in each sub feature‘s Environment.
  • The reducer owning the global state writes to the subject
  • The sub features subscribe to this global state subject via an action, and the subscription writes to the store whenever a new global state value is sent by the subject. (Re the last part I‘m not sure whether this actually works, haven’t done so much work with Effects)