Best practice for sharing data between many features?

I'm trying to set up an app that has many features that need access some shared data, in this case a User struct.

struct AppState {
  var user: User
  var featureA: FeatureAState
  var featureB: FeatureBState
}

struct FeatureAState {
  var user: User // same User from AppState
  var something = false
}

struct FeatureBState {
  var user: User // same User from AppState
  var anotherThing = true
}

enum AppAction {
  case featureA(FeatureAAction)
  case featureB(FeatureBAction)
}

What are the best practices for letting the many features access the same User while not allowing them to see more state beyond that?

1 Like

I found this case study with shared state but I am still interested to hear if there are suggestions because the only shared state in my example is the User. The case study shares state from a property on the struct but in my example the features' states have to keep track of their own data and access the shared User.

There are two ways we show how to share state in the case studies:

First, the case study you found shows that you can share state from a parent to a child via an explicit computed property. Maybe the case study doesn't properly explain the technique, but it does apply to your situation. You just need a way to hold both the core feature state and the shared state together, either as a new type or as a tuple:

struct AppState {
  var user: User
  var screenA: ScreenAState
  var screenB: ScreenBState

  var featureAState: (user: User, screen: ScreenA) {
    get { (self.user, self.screenA) }
    set { self.user = newValue.user; self.screenA = newValue.screen }
  }

  var featureBState: ...
}

struct ScreenAState {
  // no User field here
  var something = false
}

And then your reducer would operate on (user: User, screen: ScreenA) instead of just ScreenA, and your reducer would be pulled back along the \.featureAState key path instead of the \.screenA key path. You could also use a typealias or a new struct to wrap that data if that feels better. If you use a struct you could even add @dynamicMemberLookup to make accessing ScreenAs data inside the struct nicer.

You could go even further and just have a fully generic base state struct of everything you want shared:

@dynamicMemberLookup
struct BaseState<State> {
  var user: User
  var state: State

  subscript(...) { ... }
}

And that would allow you to share some state with basically every part of your app.

We actually have a case study that does exactly this, except for dependencies and the Environment. This shows how to create a SystemEnvironment that wraps another environment and will give all reducers access to a base set of dependencies.

Hopefully that is helpful!

6 Likes

This is great! The @dynamicMemberLookup approach is really nice to use. Thank you for explaining it so clearly.

I was a bit confused on why ScreenAState still has the var user: User. That isn't needed (as I am sure you are aware, but may confuse others too).

struct ScreenAState {
  var something = false
}

The approach with the generic BaseState also works nicely.

One great improvement would be if this bit (but with BaseState instead of the tuple):

var featureAState: (user: User, screen: ScreenA) {
  get { (self.user, self.screenA) }
  set { self.user = newValue.user; self.screenA = newValue.screen }
}

could be done with a property wrapper (especially if you need an optional variant of featureAState), but due to accessing the user from AppState I don't think that's possible. Or am I missing something?

Good catch! I just updated my original post.

Also the property wrapper idea sounds cool. We have not spent much time in that area. Would be interested in seeing what you come up with!

I'm trying to share the same state throughout the app, so what happens when I need to do that?

Let's say I have the above mentioned ScreenAState but I need to share the User deeper down:

struct ScreenAState {
  var something = false
  
  var screenASubState: ScreenASubState
}

struct ScreenASubState {
  var somethingElse = true
}

I would like to use the @dynamicMemberLookup method with a struct, but I would need access to User in ScreenAState.

struct ScreenAState {
  var something = false
  
  var screenASubState: ScreenASubState
  var featureASubState: BaseState<ScreenASubState>  {
    get { .init(state: screenASubState, user: ???) } 
    set {
      screenASubState = newValue.state
      ??? = newValue.user
    }
  }
}

Do I also need to pass the user down to ScreenAState? Or is there a better way of handling this?

1 Like
@dynamicMemberLookup
struct BaseState<State> {
 var user: User
 var state: State

 subscript(...) { ... }
}

@mbrandonw can you do a case study on this generic state example? I find it that in practice it's not that ergonomic when you have a heterogeneous array of such states and are forced to do casting to and from a protocol interface that servers as a shim over the generic:

struct Foo {
  let foo: [BarProtocol] // Cannot be a plain Bar because generics
}

protocol BarProtocol {}

struct Bar<State>: BarProtocol {
  let sharedProperty: String
  let state: State
}

I think it'd be best to rely on enums when dealing with distinct state cases:

@dynamicMemberLookup
struct State {
  let sharedProperty: String
  let message: Message
  
  subscript { ... } // access all Message properties for the instantiated case
}

enum Message {
  case text(Text)
  case image(Image)
  case video(Video)
}

extension Message {
  struct Text { ... } // text only properties
  struct Image { ... } // image only properties
  struct Video { ... } // video only properties
}

I'm not quite sure how to achieve this with CasePaths and @dynamicMemberLookup for enum associated values.

Have you seen the case study that applies this technique to the environment? It should be roughly the same for BaseState.

I have seen it yes, maybe I’m misunderstanding it, but as I mentioned, it’s not quite the same when you consider a heterogeneous array of states such as the chat example I outlined above.

If you have 3 types of state that can be in your list — text, image, audio — and you don’t want to rely on an IfLetStore to check for optional state for each, what are the other options?

Generics / enums are good tools for modeling heterogeneity, but I don’t see them leveraged to that end in any of the case studies.

I’d say it’s quite common for apps to display a list of varied items that have distinct but also shared properties that benefit from this sort of separation of concerns in terms of domain modeling.

Yeah, that is a very good thing to be able to accomplish, I just wasn't sure how it was related to the problem of shared state.

The crux of the problem is that you want to model state with an enum, and SwiftUI nor TCA give you good tools for picking apart that state so that you can modularize based on each case of the enum.

Luckily it can be done, but right now support for it is rather experimental. We have a branch here that demonstrates how to do this with the Tic-Tac-Toe example app, which models its root app state as an enum instead of two optional values:

public enum AppState: Equatable {
  case login(LoginState)
  case newGame(NewGameState)
}

We've been using this technique a lot in an application we are building, and we're just trying to think through all the edge cases before we publicly release it. If you give it a shot let us know how it works for you.

I see. The problem is that enums forbid declaring stored instance properties **. Which means we'll still need a struct at the shared scope level. And I think even with your experimental changes it's not possible to get @dynamicMemberLookup on the enum case associated state value, right?

** maybe static properties could work, but I think it'd a bad practice

I wrestled with your question exactly on how to handle the user property myself. (I read the shared Environment case study referenced above, but it was so dense that a got a little lost.)

Here is a gist for my implementation of BaseState. As best as I can directly answer your question, BaseState is used solely as a computed property. BaseState is used / implemented exactly as how Brandon indicates a tuple should be used to manage shared state in his initial post above. (post #4). There is no logical difference: the only difference is using a struct with generic state property, instead of a tuple. As such BaseState needs access to backing vars. For that reason, the backing vars that are get/set by the computed BaseState property have to be found in the same struct / scope / level as where the BaseState computed properties are declared. Your 'mistake' is to plant the featureASubState var in the ScreenAState struct instead of AppState struct. Since featureASubState is not a var in AppState, the getters / setters do not have access to the user property found in AppState that you wish to share. (Hence your '???'. )

Using structs for state remains a bit of a mind bender for me. Reference types make it possible to use dependency injection to reference shared state objects / properties. But with structs, it's a different mental model. Initially it's a lot more difficult for me to fathom. But I'm trusting in the deeper experience base of the PointFree guys that the trade-offs are worth it.