Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Local State nested updates can cause inconsistent UI #57

Open
fonesti opened this issue Dec 27, 2018 · 2 comments
Open

Local State nested updates can cause inconsistent UI #57

fonesti opened this issue Dec 27, 2018 · 2 comments

Comments

@fonesti
Copy link
Contributor

fonesti commented Dec 27, 2018

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_0 and rootView.update() begins
  • view_A.model = viewModel_A_0
  • view_A.update() begins
  • During view_A.update, the increment interaction is called, causing a nested update cycle (1)
    • state_0 and localState_1 are used to compute rootViewModel_1
    • rootView.model = rootViewModel_1 and rootView.update() begins
    • view_A.model = viewModel_A_1 and viewA.update() is executed
    • view_B.model = viewModel_A_1 and viewB.update() is executed
    • `rootView.update() ends
  • Update cycle 1 ends, update cycle 0 continues
  • viewA.update() is resumed (**)
  • viewA.update() ends
  • view_B.model = viewModel_B_0 and viewB.update() is executed (*)
  • `rootView.update() is resumed (***)
  • `rootView.update() ends
  • Update cycle 0 ends

Problems

At the end:

  1. View_B is one view model behind (*)
  2. If we use a copy of the view model inside view_A.update() (e.g. we use guard let model = self.model), a part of the old model can be applied on top of the latest model. (**)
  3. 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

@fonesti
Copy link
Contributor Author

fonesti commented Dec 27, 2018

A simple demo project
Demo.zip

@smaramba
Copy link
Contributor

smaramba commented Jan 3, 2019

Thanks, will look into this asap.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants