ReSwift Custom Diffs and Enqueued State Updates
Vinh Nguyen found that his ReSwift status updates became slow.
- There were too many subscribers.
- Objects would react to state updates by dispatching a new action immediately. (ReSwift action dispatching happens synchronously.)
His app state ends up containing a lot of objects in a 3-level hierarchy that mimicks the hierarchy of view components on screen. In a drawing or otherwise canvas based graphics app, it seems. It doesn’t make sense to have each objects on the canvas responds to state updates when one other object updates on screen. Instead, you’ll want to at least minimize the amount of updates that get passed through.
Vinh implemented a custom diff or “delta update” for the 2nd level in his 3-level hierarchy of objects because they were few enough to be performant during state updates, and could easily manage their child objects.
Read about his discovery of state update bottlenecks on his blog.
He solved the second problem, newState
callbacks triggering the dispatch of another action, by enqueuing the dispatch in an asynchronous block on the main queue, which is the queue ReSwift uses:
class ObjectView {
func newState(state: ObjectState) {
// ...
if conditionThatTriggersAnAction == true {
DispatchQueue.main.async {
store.dispatch(Action())
}
}
}
}
Sure, this enqueues the action dispatch until the current execution is finished. But you have to take care about other actions being dispatched in between now, and if that is a problem. (E.g. another subscriber type reacting to the same state update with another action.)
I had prefered another solution initially: subscribe to updates in the top level Canvas
object, then delegate down the view hierarchy as needed. Every sub-component that wants to fire an action tell the Canvas
about this, which enqueues the actions, and then processes the queue after all sub-component updates are finished. A bit like in game development where the game loop ensures there is just 1 point of action handling per run. But then again, Vinh’s approach does exactly that: it enqueues action dispatching until later, ensuring the current run loop run isn’t interrupted. Also, my approach to delegation would make everything just so much more complicated in the app code.
I wonder is it’d be beneficial if the ReSwift store operated on a high priority queue that is not the main queue all the time. Then you can dispatch actions synchronously from view components on the main queue, waiting for the result, or asynchronously.
I will have to think more about the consequences of an approach like this before I suggest anything to anybody, though. I don’t do a lot of concurrent programming in my apps, and when I do, I contain it very strictly; on the downside, I don’t have developed any instinct regarding implications of using multiple queues.