Dimillian / SwiftUIFlux

A very naive implementation of Redux using Combine BindableObject to serve as an example

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Stale State in Connected Views

danhalliday opened this issue · comments

I’m seeing stale states sometimes in connected views. Can’t figure it out — have removed all middleware. It looks like after an action is processed and the state is updated, map(state:dispatch:) is called with the old state. But only sometimes (perhaps 20% of the time).

func map(state: AppState, dispatch: @escaping DispatchFunction) -> Props {
    print("Latest screen:", state.diaryState.currentScreen) // Sometimes currentScreen doesn't update

I would happily dig into this or submit a PR but I’m stumped. The Store implementation looks straightforward. I can’t produce a reduced test case but at the same time there’s nothing unusual about the project I’m seeing the issue in.

Adding in an additional DispatchQueue.main.async {} into a new middleware fixes it most of the time but not always. I guess it has to be something to do with how SwiftUI is coalescing updates to the @Published public var state: StoreState store property.

This does in fact seem to be down to SwiftUI’s coalescing of updates. Getting rid of the following async dispatch in the Store fixes the problem:

public func dispatch(action: Action) {
    // DispatchQueue.main.async {
        self.dispatchFunction(action)
    // }
}

During an action update, I see the StoreConnector body getter called 3 times, but the map(state:dispatch) function only called once. With the Store’s DispatchQueue.main.async present, the order of the body getter and map function jump around and sometimes the map function is called earlier than the body getter call showing the latest state. When I remove the async dispatch, the calls are in exactly the same order every time with the map function always following the StoreConnector body getter call that has received the new state.

Still not exactly sure on the root cause but the fix seems reliable. Also FWIW my view navigation feels snappier after removing the async dispatch. Perhaps previously that was causing SwiftUI’s change coalescing mechanism or its animation system to thrash?

@Dimillian was the rationale behind the DispatchQueue.main.async call to be a 'just in case' users dispatch actions on threads other than the main thread?

Flagging the Store as dirty with the objectWillChange publisher in the dispatch block also fixes the issue (again just FWIW, I’m meddling in things I don’t fully understand!)

public func dispatch(action: Action) {
    DispatchQueue.main.async {
        self.objectWillChange.send()
        self.dispatchFunction(action)
    }
}

I have the same problem but cannot figure out exactly why.