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?

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!

3 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!

Terms of Service

Privacy Policy

Cookie Policy