DEV Community

Discussion on: The Cost of Consistency in UI Frameworks

Collapse
 
ninjin profile image
Jin • Edited

I would call Vue's lazy computation approach "invisible inconsistency". What's inside doesn't really matter, as long as everything looks consistent to the outside observer.

When you access the reactive properties programmatically, the desired part of the subgraph is updated on the fly.

Yes, if you access the DOM directly, you get inconsistency in Vue. But that's a Vue-specific problem that Vue can easily solve. For example, in $mol_view, each component has two methods: dom_node that returns the current DOM element as is, and dom_tree that returns the same DOM element, but ensures all its subtree is up to date.

@mem count( next = 0 ) { return next }

@mem doubleCount() { return this.count() * 2 }

@act test() {

    this.count( this.count() + 1 )

    console.log( this.count() ) // 1
    console.log( this.doubleCount() ) // 2
    console.log( this.dom_node().textContent ) // "0"
    console.log( this.dom_tree().textContent ) // "2"

}
Enter fullscreen mode Exit fullscreen mode

The user will not see an inconsistent state either, because the whole reactive state tree will already be updated automatically by the time it is rendered. In $mol_wire, we go one step further and don't do the full update at the end of the current event handler, but postpone it until the next animation frame. Thus recalculations caused by different events for a short time (about 16ms), when the user will not see their result, are not performed unnecessarily.

The Solid model has exactly the opposite approach, resulting in low default performance, and consequently the need for additional gestures for optimization. But the worst thing is not even that, but that exceptional situations at least break consistency, and at most break the application (although the second seems to be just a critical bug):

const [count, setCount] = createSignal(0)
const doubleCount = createMemo( ()=> count()%2 ? aBug() : count()*2 )
const increment = () => setCount(count() + 1)

console.log( count(), doubleCount() ) // 0 0 OK

try { increment() } catch {}
console.log( count(), doubleCount() ) // 1 0 Inconsistent, exception expected

increment() 
console.log( count(), doubleCount() ) // 2 4 OK

// After 100 clicks outputs: 4
return (
  <button type="button" onClick={increment}>
    {doubleCount()}
  </button>
)
Enter fullscreen mode Exit fullscreen mode
Collapse
 
ryansolid profile image
Ryan Carniato

My biggest concern with it is the example where I show Vue imitating Svelte. I don't think things that should be derived should be modelled that way, but when people do that inevitably out of laziness it's awkward because part of it has updated and other parts haven't. With React or Svelte you at least know that nothing downstream has run.

Yeah that seems like a bug if it never picks up again. I think I know when I broke that. Thanks for the heads up.

Out of curiousity though what do you expect to happen around the first error case. In Vue or Solid or any reactive library count() is set by that point and doubleCount couldn't be calculated. Should it throw on read?

Collapse
 
ninjin profile image
Jin • Edited

For this reason, no separate effects are used when using $mol_wire. Instead, the same invariants are used that describe the usual reactive dependencies as well, but neither can change something on the side besides the return value. This gives a stable, predictable and manageable moment of applying side effects.


@mem count( next = 1 ) { return next }
@mem doubleCount () { return this.count() * 2 }

@mem countLog() {
    console.log( 'count', this.count() )
}

@mem doubleCountLog() {
    console.log( 'doubleCount', this.doubleCount() )
}

@mem render() {
    console.log( '[' )
    this.doubleCountLog()
    this.countLog()
    console.log( ']' )
}

@act main() {

    // initial
    this.render() // logs: [ 2 1 ]

    // change state
    this.count( 2 ) // no logs
    this.count( 3 ) // no logs

    // don't wait next frame
    this.render() // logs: 6 3

    // check memoizing
    this.render() // no logs

}
Enter fullscreen mode Exit fullscreen mode

Sandbox

$mol_wire and MobX have similar exception behavior - they are transparent to them as [if there were no intermediate memoization and the computation starts from scratch every time they are accessed] - this gives a simpler mental model.

@mem count( next = 0 ) { return next }

@mem doubleCount () {
    return this.count()%2 ? aBug() : this.count()*2
}

@act increment() {
    this.count( this.count() + 1 )
}

@act main() {

    console.log( this.count(), this.doubleCount() ) // 0 0 OK

    this.increment()
    console.log( this.count(), maybe( ()=> this.doubleCount() ) ) // 1 ReferenceError OK

    this.increment() 
    console.log( this.count(), this.doubleCount() ) // 2 4 OK

}

Enter fullscreen mode Exit fullscreen mode

Sandbox

Thread Thread
 
ryansolid profile image
Ryan Carniato

Thanks this is good information. I appreciate you taking the time to explain.

Collapse
 
eshimischi profile image
eshimischi • Edited

Dude, calm down with you $mol , habr is fed up with your mess already. When you’ll stop imposing your craft on everyone, maybe someone will find interest in it, but for now you are just an annoying hypocrite. I checked your $mol, bad technics, spaghetti code and it deffo not even comparable to Big Three Libraries/Frameworks.. stop it, please.

Collapse
 
ninjin profile image
Jin

Could you describe your complaints about $mol in more detail so that I can correct them? Where did you find the spaghetti there? Which practices seem bad to you and why?