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.