-
Notifications
You must be signed in to change notification settings - Fork 33
Local State nested updates can cause inconsistent UI #57
Description
The issue is about LocalState updates performed inside a ModellableView's update() method.
Example
This is an example Tempura UI
ViewControllerWithLocalState
|
|
RootView
| |
| |
View_A View_B
View_A and View_B inherently depends on LocalState.
View_A has an interaction that updates the LocalState
class AppViewController: ViewControllerWithLocalState<RootView> {
override func setupInteraction() {
self.rootView.view_A.increment = { [unowned self] in
self.localState.counter += 1
}
}
}As local state updates are synchronous, an interaction call inside View_A.update(oldModel:) can cause unordered nested updates and unexpected bugs 😱.
Let's say a generic state change has started an update cycle (0):
state_0 and localState_0 are used to compute rootViewModel_0, and inherently viewModel_A_0 and viewModel_B_0
rootView.model = rootViewModel_0androotView.update()beginsview_A.model = viewModel_A_0view_A.update()begins- During
view_A.update, theincrementinteraction is called, causing a nested update cycle (1)state_0andlocalState_1are used to computerootViewModel_1rootView.model = rootViewModel_1androotView.update()beginsview_A.model = viewModel_A_1andviewA.update()is executedview_B.model = viewModel_A_1andviewB.update()is executed- `rootView.update() ends
- Update cycle 1 ends, update cycle 0 continues
viewA.update()is resumed (**)viewA.update()endsview_B.model = viewModel_B_0andviewB.update()is executed (*)- `rootView.update() is resumed (***)
- `rootView.update() ends
- Update cycle 0 ends
Problems
At the end:
- View_B is one view model behind (*)
- If we use a copy of the view model inside
view_A.update()(e.g. we useguard let model = self.model), a part of the old model can be applied on top of the latest model. (**) - The same considerations of 2 apply to
RootView(***)
- While debugging, this behaviour is counter-intuitive in respect to the normal update cycle
- Usually view hierarchies are more complex
- There could be more levels of nested updates
Finding and fixing these kinds of bugs can be just difficult and really time consuming 🤕
Possible Solutions
I see two way to address this issue:
- consider changing the local state during a model update an anti-pattern or a programming error and perform an assertion.
- Is this kind of setup ever useful (maybe to interact with some UI related framework with observers/callbacks/delegates) or it is just the consequence of a bad design?
- enqueue incoming LocalState to avoid nested update cycle
I'm not keen on a particular solution, both seems easy to implement.
┆Issue is synchronized with this Asana task by Unito