Video #73: Modular State Management: View State
Episode: Video #73 Date: Sep 23, 2019 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep73-modular-state-management-view-state

Description
While we’ve seen that each reducer we’ve written is super modular, and we were easily able to extract each one into a separate framework, our views are still far from modular. This week we address this by considering: what does it mean to transform the state a view has access to?
Video
Cloudflare Stream video ID: c72f5b382f031ec832085fe00e28e3e1 Local file: video_73_modular-state-management-view-state.mp4 *(download with --video 73)*
References
- Discussions
- Why Functional Programming Matters
- Access Control
- Elm: A delightful language for reliable webapps
- Redux: A predictable state container for JavaScript apps.
- Composable Reducers
- 0073-modular-state-management-view-state
- Brandon Williams
- Stephen Celis
- Mastodon
- GitHub
- CC BY-NC-SA 4.0
- source code
- MIT License
Transcript
— 0:06
The ease at which we modularized these reducers speaks to just how modular this architecture is by default. We didn’t have to make any striking refactors or changes to logic because the boundaries are already very clearly defined. The worst things got was the need to introduce a little extra boilerplate for a component that had more complex state. Modularizing our views
— 0:44
While our architecture has seriously simplified our views, it still doesn’t have a story for modularizing them. We have isolated our reducers from the entirety of app state and app actions, but we have not yet isolated our views. So let’s start chipping away at that part of the problem.
— 1:05
All of our views have access to all of app state, and they all have the ability to send any app action. This is because they all hold onto a central Store that is generic over AppState and AppAction . Whenever we pass this central store to a view, we give that view way more information and power than it needs. We also have no way to look at a view and, at-a-glance, understand what information it has access to and what mutations it’s capable of making.
— 1:35
For example, we have to scan the entire body of FavoritePrimesView to know that it only needs access to the array of favoritePrimes and the deleteFavoritePrimes action, yet it has access to everything. struct FavoritePrimesView: View { … }
— 1:47
This means that nothing is preventing us from leaking more global state and global actions into this view. In fact, it’s very easy to do!
— 1:54
For instance, we could send the counter screen’s incrTapped action when delete is called: .onDelete { indexSet in self.store.send(.favoritePrimes(.deleteFavoritePrimes(indexSet))) self.store.send(.counter(.incrTapped)) }
— 2:05
And though this can seem like a silly example, these kinds of unrelated mutations can easily sneak in during copy-paste refactorings. Ideally, this view would live in a module, completely isolated from global state and actions so that the compiler can catch these kinds of mistakes for us.
— 2:19
If we scroll up further, we need to read through IsPrimeModalView to understand that it only needs a couple parts of AppState : the current count and the array of favoritePrimes . And it only needs a couple modal actions. struct IsPrimeModalView: View { … }
— 2:41
And there’s a lot going on in CounterView : struct CounterView: View { … }
— 3:02
What we are seeing here is that although our reducers have been refactored to take the bare minimum needed to get their job done, the views do not have this nice quality. We want to refactor these screens so that they take state and actions that more closely resemble the reducers that represent their behavior.
— 3:15
So how do we solve this problem? Well they all have this line in common: @ObservedObject var store: Store<AppState, AppAction>
— 3:27
If we were somehow able to pass a more specific store that works on just the state and actions a view cares about, not only would it make them easier to understand, it would unlock the ability to extract them to their own modules and isolate them from the rest of our application. But how can we do that?
— 3:41
Well, this isn’t our first rodeo! We have seen time and time again that by focusing on small, composable, transformable units, we can unlock all kinds of possibilities. So far our focus has been on the compositionality of the reducer function, but what about the Store type? Can we transform the Value and Action of a Store so that a global store can be transformed into a more local store? Well, the answer is yes! Transforming a store’s value
— 4:06
Let’s start by looking at how we can transform a store’s value.
— 4:09
Right now the only way in which we interact with the store’s value is to get it via the property. This is because the value field has a private setter, and so outside the ComposableArchitecture module we literally can’t do anything but get the value. What we want is to have a transformation automatically applied to the store so that whenever we try to access value we only get the parts of the app state that we actually care about.
— 4:42
Let’s explore this by stubbing out a method on Store with the aim to do just that. I’m not sure what to call it yet, so let’s use underscores so that we can focus entirely on the work we want it to do. public final class Store<Value, Action>: ObservableObject { … func ___
— 4:50
We know we want this method to return another Store : public final class Store<Value, Action>: ObservableObject { … func ___() -> Store
— 4:54
We also know we want to transform the current store’s value into something more local, so let’s introduce a generic. public final class Store<Value, Action>: ObservableObject { … func ___<LocalValue>() -> Store
— 5:01
The returned Store will be generic over this LocalValue , and Action , which will remain unchanged. public final class Store<Value, Action>: ObservableObject { … func ___<LocalValue>() -> Store<LocalValue, Action>
— 5:08
So let’s open the body of this function and look at the working parts. We know we need to return a brand new Store<LocalValue, Action> , so let’s call its initializer. public final class Store<Value, Action>: ObservableObject { … func ___<LocalValue>() -> Store<LocalValue, Action> { return Store<LocalValue, Action>( initialValue: <#LocalValue#>, reducer: <#(inout LocalValue, Action) -> Void#> ) }
— 5:20
The Store initializer takes an initial value and a reducer. In order to provide an initial value, we need to somehow transform Value into LocalValue . Sounds like we need a function! Let’s introduce one as an argument. public final class Store<Value, Action>: ObservableObject { … func ___<LocalValue>(_ f: (Value) -> LocalValue) -> Store<LocalValue, Action> { return Store<LocalValue, Action>( initialValue: <#LocalValue#>, reducer: <#(inout LocalValue, Action) -> Void#> ) }
— 5:51
And now we can take the current store’s self.value and apply f to transform it from a Value into a LocalValue . public final class Store<Value, Action>: ObservableObject { … func ___<LocalValue>(_ f: (Value) -> LocalValue) -> Store<LocalValue, Action> { return Store<LocalValue, Action>( initialValue: f(self.value), reducer: <#(inout LocalValue, Action) -> Void#> ) }
— 5:59
But what about the reducer ? How can we implement this? Well, we can at least open up the closure, where we have access to a LocalValue and an Action . public final class Store<Value, Action>: ObservableObject { … func ___<LocalValue>(_ f: (Value) -> LocalValue) -> Store<LocalValue, Action> { return Store<LocalValue, Action>( initialValue: f(self.value), reducer: { localValue, action in } ) }
— 6:11
It was easy to transform the store’s value using f , but transforming the store’s reducer seems impossible. The things we have at our disposal don’t seem to fit together. For example, we have a local value, but our function transforms global values to local values, so that doesn’t help us.
— 6:28
And indeed, in some sense it is impossible to implement this function. To clear away some noise, let’s paste in the actual signature we are trying to implement: func transform<A, B, Action>( _ reducer: (inout A, Action) -> Void, _ f: (A) -> B ) -> (inout B, Action) -> Void { fatalError() }
— 6:38
We discuss why this kind of function is impossible to implement in our episode on contravariance , where we discussed the idea of positive and negative positions of type parameters in functions. We highly encourage everyone to go watch that episode, but what it boiled down to is that in order to transform a type parameter in a function signature, it must either be in positive position or negative position. However, the A parameter here is in both positive and negative positions. This is because, as we have mentioned a few times, a function that takes an inout parameter is kind like a function that returns a whole new value.
— 7:24
So really, this function signature is equivalent to: func transform<A, B, Action>( _ reducer: (A, Action) -> A, _ f: (A) -> B ) -> (B, Action) -> B { fatalError() }
— 7:30
Now we see that A appears on both sides of the function arrow, and hence it is not possible to implement this function.
— 7:38
That was a very brief description of the problem, but again we highly recommend you watch our episode on contravariance if you want a deeper understanding of what is going on here.
— 7:47
So what are we going to do if we can’t transform our existing reducer? Well, we actually have more information at our disposal here, it’s just a bit hidden. We have our reference to self , and it has a send method. We can pass the available action to that method: reducer: { localValue, action in self.send(action) }
— 8:09
This will cause self.value to mutated, which we can transform into a local value using f . reducer: { localValue, action in self.send(action) let updatedLocalValue = f(self.value) }
— 8:23
And remember, localValue is a mutable, in-out variable, so we can reassign it with our updated local state. reducer: { localValue, action in self.send(action) let updatedLocalValue = f(self.value) localValue = updatedLocalValue }
— 8:36
We also need to mark f escaping since it’s now being captured by the store. func ___<LocalValue>( _ f: @escaping (Value) -> LocalValue ) -> Store<LocalValue, Action> {
— 8:44
We can even simplify things by doing that work on a single line. reducer: { localValue, action in self.send(action) localValue = f(self.value) }
— 8:49
So it now compiles, but things are a little strange.
— 8:55
We couldn’t implement the reducer using just the data we had at hand, we had to turn to the internal behavior of the store by sending an action to the store and then relying on the fact that the store mutated its value in the process.
— 9:09
Also, we didn’t even really use the localValue provided to us in this reducer. We only used it as a way to overwrite it with the new local value. That seems odd. A familiar-looking function
— 9:23
But strangeness aside, at least it’s compiling now. Further, the shape may look pretty familiar.
— 9:38
If we rewrote it as a function it might look something like this: ((Value) -> LocalValue) -> ((Store<Value, _>) -> Store<LocalValue, _>
— 10:05
And if we abstract away the generic names with simpler A s and B s. ((A) -> B) -> ((Store<A, _>) -> Store<B, _>)
— 10:17
And if we take things a step further, we could even abstract away the Store for another generic container. ((A) -> B) -> ((F<A>) -> F<B>)
— 10:33
This shape may be very familiar by now to some of our viewers. It’s map ! map: ((A) -> B) -> ((F<A>) -> F<B>)
— 10:37
The map function is an operation we first explored in depth way back in our thirteenth episode . In it we explored how map is an operation that allows us to lift a function between types up to a function between generic types. The Swift standard library defines map on arrays and optionals. It even defines it twice on the result type, one for each generic: success and failure. We’ve also explored defining map on our own types, including lazy values, parallel values, validated values, generators of random values, even parsers.
— 11:09
Have we found another map on Store ? public final class Store<Value, Action>: ObservableObject { … func map<LocalValue>( _ f: @escaping (Value) -> LocalValue ) -> Store<LocalValue, Action> { return Store<LocalValue, Action>( initialValue: f(self.value), reducer: { localValue, action in self.send(action) localValue = f(self.value) } ) } }
— 11:15
Let’s take things for a spin and open up a playground to see how this map works.
— 11:21
We can start by creating a very simple store around an integer and a single Void action that increments it. let store = Store<Int, Void>( initialValue: 0, reducer: { count, _ in count += 1 } )
— 11:54
Then we can send a few Void s actions along and see how it changes the store’s value. store.send(()) store.send(()) store.send(()) store.send(()) store.send(()) store.value // 5
— 12:05
So far this behaves exactly how we would expect.
— 12:07
Now let’s create a new store by mapping on this one with the identity function, which does not change the value: let newStore = store.map { $0 } newStore.value // 5
— 12:33
We can start sending the new store actions and check its value at the end. newStore.send(()) newStore.send(()) newStore.send(()) newStore.value // 8
— 12:41
If we look back at the original store’s value, however, we’ll see something strange: store.value // 8
— 12:46
It has for some reason incremented to 8 as well, even though we didn’t send any actions to it. The reason is simple enough: the original store and new store are not distinct entities, but rather are inextricably linked. The derived store holds onto a reference of the original store, and mutates it whenever we send an action.
— 13:05
This definitely feels a bit strange, but it’s also the behavior we want: whenever a local store is sent an action, we want the global store to be notified so that the local change can be represented globally in our application.
— 13:34
Unfortunately things are not quite right yet. To see the problem, we can send actions to the root store: store.send(()) store.send(()) store.send(())
— 13:49
While the root store’s value increments, the new store’s value has not: newStore.value // 8 store.value // 11
— 13:56
A local store communicates local changes back to the global store because it calls the global store’s send method directly. But a local store has no means of receiving updates from the global store. If a local store could somehow observe updates to its root store, we could propagate these global changes locally. What’s in a name?
— 14:17
We can solve this problem, and we will in a moment, but before we do, let’s reflect a little bit on what we just did. Although, we’ve now implemented a method that can transform a store that works with global state into a store that works with local state, and it seems to have the shape of the map function, this function is not like any of the other map functions we’ve encountered on Point-Free.
— 14:38
In our original episode on the map function we showed that it is a very mathematical concept, and it even satisfies a uniqueness property. What this means is that if your map satisfies a simple property, then that will uniquely determine map on that type amongst all possible functions with map ’s signature. In essence, map is something uniquely determined by our type and not something we just get to pick and choose how we want to implement.
— 15:03
However, we can only make sense of these statements and results if the types we are mapping on are pure constructions, because that is what we have in the mathematics world.
— 15:12
And unfortunately, the Store type is a class with lots of behaviors bundled into it. It isn’t defined by just simple data like a value type is, it holds onto mutable data that changes over time, and exposes methods that aid in mutating that data. All of these things exist outside the reach of the mathematical formulation of map , which is what gives map a lot of its powers.
— 15:33
And this observation manifests itself in some real world confusion, like how changes to a local store affect its global store, and how changes to a global store affect any local stores! This is a type of behavior we have not observed in any of our other map operations.
— 15:54
Imagine if we have an array of integers and form a new array by mapping over the original with the identity function: var xs = [1, 2, 3] var ys = xs.map { $0 }
— 16:08
What if we then appended the result with another number? ys.append(4)
— 16:14
If we inspect each value, they differ: xs // [1, 2, 3] ys // [1, 2, 3, 4]
— 16:22
If the xs array had been mutated we would find that very surprising, and would introduce a lot of potential complexity in using map on arrays.
— 16:36
But what we are witnessing with the Store type is that exact situation, where somehow, by mutating the local store, we are secretly mutating the global store.
— 16:47
So, due to what we are observing here, we do not feel comfortable calling this operation map . In general, we would like to encourage everyone to exercise caution when defining map and related operations on reference types because they can lead to quite complex objects that many times do not behave how you would expect, and so don’t have the same intuitions. You are of course free to define map on your generic reference types, no one is going to stop you. It just won’t behave like the map s we’ve defined on Point-Free and might lead to confusing results.
— 17:18
This isn’t to say that you can’t sometimes control a reference type in such a way that it makes sense to define map on it, just that doing so is inherently more complicated than it is for simple value types.
— 17:32
So instead of calling this operation map , we feel more comfortable calling it by a more domain-specific name: view . We like this name because it returns a “view” into a store, where we are only allowed to look at a local version of the global value. public final class Store<Value, Action>: ObservableObject { … func view<LocalValue>( _ f: @escaping (Value) -> LocalValue ) -> Store<LocalValue, Action> { return Store<LocalValue, Action>( initialValue: f(self.value), reducer: { localValue, action in self.send(action) localValue = f(self.value) } ) } } Propagating global changes locally
— 17:56
We now have a function that allows us to transform global stores into local ones, but local stores still have a problem: they aren’t notified of changes to the global stores they’re derived from.
— 18:19
Stores are already observable due to the SwiftUI machinery they hook into, and this is exactly what has allowed store changes to be reflected, immediately, in SwiftUI views.
— 18:37
Store conforms to the ObservableObject protocol and its value is wrapped with the @Published property wrapper. final class Store<Value, Action>: ObservableObject { … @Published public private(set) var value: Value
— 18:49
This protocol and property wrapper come from Apple’s new Combine framework, and using them synthesizes objects that can publish changes to anyone interested, including SwiftUI views.
— 18:58
SwiftUI views can wrap these ObservableObject s, like our store, using the @ObservedObject property wrapper: @ObservedObject var store: Store<AppState, AppAction>
— 19:05
Even though these publishers are kind of hidden away, we still have access to them. For example, by conforming to ObservableObject , our store automatically gets an objectWillChange publisher. self.objectWillChange // ObservableObjectPublisher
— 19:21
This is what SwiftUI subscribes to in order to snapshot its current state and re-render things efficiently by determining what changes.
— 19:28
The @Published property wrapper, meanwhile, introduces a Combine publisher of store value updates. It can be accessed by prepending value with $ . self.$value // Published<Value>.Publisher
— 19:41
Any interested party can subscribe to a Combine publisher via its sink method, which takes a callback closure that is invoked whenever a new value comes in. self.$value.sink(receiveValue: <#((Value) -> Void)#>)
— 19:52
This means that when we create a local store, we can notify it of global updates by subscribing to the global store’s value publisher and passing along these updates accordingly.
— 20:02
In order to notify the local store let’s capture it in a reference before returning it. func view<LocalValue>( _ f: @escaping (Value) -> LocalValue ) -> Store<LocalValue, Action> { let localStore = Store<LocalValue, Action>( initialValue: f(self.value), reducer: { localValue, action in self.send(action) localValue = f(self.value) } ) return localStore }
— 20:11
Next, we can call sink on the root store’s value publisher. let localStore = Store<LocalValue, Action>( initialValue: f(self.value), reducer: { localValue, action in self.send(action) localValue = f(self.value) } ) self.$value.sink(receiveValue: <#((Value) -> Void)#>) return localStore
— 20:20
And when we open up the subscription block, we can feed every new value into the local store using the transform function. self.$value.sink { newValue in localStore.value = f(newValue) }
— 20:31
And remember, this value setter is still completely private to the Store , and nothing outside of the module can update a store’s value directly. Result of call to ‘sink(receiveValue:)’ is unused
— 20:44
We do get a warning because the sink method returns what Combine calls a “cancellable.” This is an object that can tear down a subscription, and will even do so automatically when the cancellable is deinitialized.
— 21:02
So we need ensure this return value is retained for as long as the local store is retained. One thing we can do is retain the cancellable directly on the local store by introducing an optional property. final class Store<Value, Action>: ObservableObject { let reducer: (inout Value, Action) -> Void @Published private(set) var value: Value private var cancellable: Cancellable?
— 21:23
This allows us to assign the cancellable directly when we first subscribe. localStore.cancellable = self.$value.sink { newValue in localStore.value = f(newValue) }
— 21:39
But we need to be careful here. This code has introduced a retain cycle. The local store is retaining a cancellable that retains the local store. This means the local store is a leak and will never be released from memory.
— 22:02
We can break this retain cycle by weakifying the local store in the block. localStore.cancellable = self.$value .sink { [weak localStore] newValue in localStore?.value = f(newValue) }
— 22:20
With this subscription in place, it’s our hope that local stores will be notified of any changes to the global store, and if we hop back over to our playground, we can confirm that this is the case! store.send(()) newStore.value // 11 store.value // 11
— 22:28
The new store’s value has updated as we’d expect following an action sent to its parent store. Focusing on view state
— 22:38
We’ve now seen how Store is a transformable type by defining a function that transforms global stores into more local stores, and we’ve ensured that these local stores not only propagate local changes to their global counterparts, but their global counterparts broadcast changes to their local children.
— 22:57
Now that we have this tool, let’s use it in order to make our views take less than the entire world of AppState .
— 23:12
Let’s try updating our FavoritePrimesView to work with a Store<[Int], AppAction> . struct FavoritePrimesView: View { @ObservedObject var store: Store<[Int], AppAction>
— 23:27
The store’s value is now that array of favorite primes, so we no longer have to reach through a layer of app state. ForEach(self.store.value, id: \.self) { prime in
— 23:37
And then, in our ContentView , we need to pass a view of the root store’s favorite primes to the FavoritePrimesView . NavigationLink( "Favorite primes", destination: FavoritePrimesView( store: self.store.view { $0.favoritePrimes } ) )
— 24:06
Everything compiles, and when we add a few favorite primes, and navigate to the favorite primes screen, they show up, just as before. But now our FavoritePrimesView has much more limited access to state. It can’t ask for the current count, the current user, the activity feed, or anything else. It still takes all of AppAction , but we’ll address that in a moment.
— 24:20
So what about IsPrimeModalView ? It only needs the current count and the list of favorite primes, which is exactly what we created PrimeModalState for. struct IsPrimeModalView: View { @ObservedObject var store: Store<PrimeModalState, AppAction>
— 24:49
As we saw when we refactored the prime modal reducer to use this state, nothing in the body needs to change, but it no longer has access to the rest of app state.
— 25:00
When we instantiate IsPrimeModalView , we can pass it a view of the root store’s prime modal state. IsPrimeModalView( store: self.store.view { ($0.count, $0.favoritePrimes) } )
— 25:28
Finally we have CounterView , which uses the count directly but also needs to produce a view of a store that contains the count and favoritePrimes in order to pass that along to the prime modal view.
— 25:41
So, the CounterView works on the same state as the IsPrimeModalView , which currently is a tuple, so I suppose we can follow that pattern: typealias CounterViewState = (count: Int, favoritePrimes: [Int]) struct CounterView: View { @ObservedObject var store: Store<CounterViewState, AppAction> … } The CounterView compiles just fine, but we have to compiler errors to fix. First, when passing a store off to the prime modal from the counter view we no longer need to perform a view into the store since both views use the same state: .sheet(isPresented: self.$isPrimeModalShown) { IsPrimeModalView(store: self.store) }
— 26:09
And then in the content view, where we have the button that drills down into the counter screen, we need to perform a view on the store to pass along the appropriate state: NavigationLink( "Counter demo", destination: CounterView( store: self.store .view { ($0.count, $0.favoritePrimes) } ) )
— 26:35
And just like that our views now take a smaller subset of state than the full AppState , which means the stores that power the views are starting to look a lot more like the reducers that power them.
— 26:49
It’s also worth noting how simple these changes were, and how quick we made them. We merely changed the state that the views operated on to focus in on just what they needed, and then we made sure that the stores we passed to those views were transformed in order to pluck out these values. Till next time
— 27:09
We’ve now improved the modularity and ergonomics of accessing state in views that use the observable Store object, but we can’t quite extract any of our views to isolated modules because every view’s store still depends on AppAction . So it sounds like we need another operation to transform stores that send global actions into stores that send local actions. We found that we can transform a store’s value, so can we also transform a store’s action?
— 27:44
First, what does that mean? Right now the only time we externally deal with actions and the store is when we are sending an action to the store, by calling the send method. We would love if we could send local actions to that method instead of global ones, and then somewhere in the store maybe it can automate the process of wrapping up local actions into the global actions. Next time! References The Many Faces of Map Brandon Williams & Stephen Celis • Apr 23, 2018 Note Why does the map function appear in every programming language supporting “functional” concepts? And why does Swift have two map functions? We will answer these questions and show that map has many universal properties, and is in some sense unique. https://www.pointfree.co/episodes/ep13-the-many-faces-of-map Contravariance Brandon Williams & Stephen Celis • Apr 30, 2018 We first explored the concept of “positive” and “negative” position of function arguments in our contravariance episode. In this episode we describe a very simple process to determine when it is possible to define a map or pullback transformation on any function signature. Note Let’s explore a type of composition that defies our intuitions. It appears to go in the opposite direction than we are used to. We’ll show that this composition is completely natural, hiding right in plain sight, and in fact related to the Liskov Substitution Principle. https://www.pointfree.co/episodes/ep14-contravariance Why Functional Programming Matters John Hughes • Apr 1, 1989 A classic paper exploring what makes functional programming special. It focuses on two positive aspects that set it apart from the rest: laziness and modularity. https://www.cs.kent.ac.uk/people/staff/dat/miranda/whyfp90.pdf Access Control Apple This chapter of the Swift Programming Language book explains access control in depth and how it affects module imports. https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html Elm: A delightful language for reliable webapps Elm is both a pure functional language and framework for creating web applications in a declarative fashion. It was instrumental in pushing functional programming ideas into the mainstream, and demonstrating how an application could be represented by a simple pure function from state and actions to state. https://elm-lang.org Redux: A predictable state container for JavaScript apps. The idea of modeling an application’s architecture on simple reducer functions was popularized by Redux, a state management library for React, which in turn took a lot of inspiration from Elm . https://redux.js.org Composable Reducers Brandon Williams • Oct 10, 2017 A talk that Brandon gave at the 2017 Functional Swift conference in Berlin. The talk contains a brief account of many of the ideas covered in our series of episodes on “Composable State Management”. https://www.youtube.com/watch?v=QOIigosUNGU Downloads Sample code 0073-modular-state-management-view-state Point-Free A hub for advanced Swift programming. Brought to you by Brandon Williams and Stephen Celis . Content Become a member The Point-Free Way Beta previews Gifts Videos Collections Free clips Blog More About Us Community Slack Mastodon Twitter BlueSky GitHub Contact Us Privacy Policy © 2026 Point-Free, Inc. All rights are reserved for the videos and transcripts on this site. All other content is licensed under CC BY-NC-SA 4.0 , and the underlying source code to run this site is licensed under the MIT License .