Communication between global and local state #401
lukeredpath
started this conversation in
General
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
I'm interested to see how people are handling the communication of events in more global state into more local states, specifically about where the responsibility for certain state changes or effects lies.
To take a concrete example, lets say we want to be able to perform certain actions when the app finishes launching. We can define a general action for that:
We can dispatch this using a
ViewStore
in ourAppDelegate
and now have a nice action to hook into. The question now is, where should we hook into this? Let's imagine we have multiple features:Now let's imagine that we want to pre-fetch some data for each feature by making an API request when the app finishes launching. There are a few ways of doing this that I want to explore and see if there's a way that is better than any other.
Option 1: Do everything in the App reducer
Because this behaviour is triggered by an
AppAction
a simple option is to do everything at the app level:The obvious upside of this approach is that you only need to look in a single place to see what your app does when it finishes launching. The downside is that this reducer case will need to be modified and grow as you add new behaviour when the app finishes launching. It also arguably means your app reducer has more knowledge about each feature than it really needs - it needs to know what effects to return, what actions they should be mapped to within each state etc. I say arguably because maybe its OK for the app reducer to have this kind of knowledge of its sub-features?
Option 2: Have each feature define it's own app lifecycle reducer
This approach has each feature define a "lifecycle" reducer that is generic over it's own state, but over
AppAction
:The pro of this approach is that each feature (which could be entirely within its own module, e.g. in a Swift package) is responsible for defining its own lifecycle behaviour reducers which are just composed into the main app reducer. This reduces the app domain's knowledge of each feature domain. On the other hand, it does mean feature modules now have a dependency on
AppAction
which could be awkward when trying to decouple your app's modules.It can also result in strange behaviour when working with optional state - for example, if
featureTwoState
onAppState
was actually optional, the lifecycle reducer can end up receiving actions when its state isnil
. Normally we'd pullback reducers of optional state using the.optional()
operator which will actually result in an assertion being raised if an action is received when optional state isnil
. This is because this is normally the result of a logic error - some effect not being cancelled or a view store sending an action when it shouldn't, but in this case it would be quite normal to receive an action when the feature state isnil
. To solve this, we must abandon the use of.optional()
and define the lifecycle reducer as being generic over the optional state and explicitly checking fornil
:It's not the worst thing in the world but it's not very obvious and does not fit with the current pattern for handling optional state within the app.
Option 3: Feature-specific lifecycle actions
This is a hybrid of the above approaches, where each feature is responsible for handling their own lifecycle actions which are triggered from the app reducer as an effect. So for example, both feature one and feature two would define an
appFinishedLaunching
action:Now to avoid having to send each of these actions from the app delegate, we still just send a single
AppAction.appFinishedLaunching
action - the app reducer is now responsible for triggering each feature's lifecycle action by using anEffect
:Feature one and two would now handle this action as part of their normal reducer. This approach shares some of the advantages of both of the above approaches - we can see from our app reducer which features have some behaviour on app launch (albeit not the actual behaviour) and each feature can handle these actions without any dependency on
AppAction
. This approach still doesn't gracefully handle the optional state problem - you would need still either need to define a separate lifecycle reducer that handles the optional state as shown above, or have the app reducer check for the optional state and only send actions to features that are non-nil (I would lean towards the latter).Conclusion
My gut feeling is option 1 is the simplest and most obvious solution of all the above. My concerns about the growth of this reducer case growing unbounded might be nothing.
Option 2 is a solution I have actually used in production code. It worked, but the optional checks made it messy. It also made it really awkward to test - in order to test these lifecycle actions, we need to define our
TestStore
in terms ofAppAction
which means having to explicitly wrap all of our feature actions in.featureXXX()
which hinders readability.Option 3 doesn't seem like a bad solution but is using an effect to trigger actions between different domains in this way a valid approach?
Beta Was this translation helpful? Give feedback.
All reactions