crank: this.refresh() failed to update DOM

I wanted

I tried to use Crank with MobX, and then I wrote observer() adopter for Crank.

I wrote

https://codesandbox.io/s/crank-router-7f5o3?file=/src/observer.ts

I got

Every invoking of this.refresh() created correct VDOM node, but the real DOM updated nothing…

image

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 2
  • Comments: 22 (5 by maintainers)

Most upvoted comments

@benjamingr Although Iterables are Pull mode, combining it with Crank, makes source code linear, and then Crank will update components automatically:

async function* Component() {
    const stream = makeStream()

    for await (const state of stream)
        yield <div>{state}</div>;
}

If we use typical Observables, manual updating must be done:

async function* Component() {
    var state;

    observable.subscribe(data => {
        state = data;
        this.refresh();
    });

    while (true)
        yield <div>{state}</div>;
}

In my opinion, the target of Generator & Async syntax is reducing count of Callbacks.

@TechQuery this is cool. I’ve been working on a router implementation myself using the history module that powers react router and works in the browser and node https://github.com/ReactTraining/history. I’ll take a look through your module and will raise some issues/PRs if you’d like!

This is my initial router code https://codesandbox.io/s/charming-goodall-p2mo7

@TechQuery yes and sorry that comment by me was kind of rude. “Misses the point” isn’t a very charitable way to speak,

Rx is a push based abstraction (like MobX and Vue observables and they are actually the same thing).

Async iterators are iterable they are a pull based abstraction. I go a bit into the differences here (I also have the slides) - but basically:

  • Observables like Rx and MobX are push based. You subscribe to them once (by calling .subscribe) and then the producer has all the control. They push data to you whenever they want. They are (as Erik who invented them told me) “hyper strict” - and the opposite of lazy.
  • Iterables are lazy, very lazy - the consumer has all the control. With an async iterator the consumer has all the control. They decide when to ask for the next item - it is not “pushed” to them.

The advantage of observables and “push based” is that the fact it’s simpler and the producer has more control. Pull based means things support back-pressure and building pipelines is possible.

Namely, any implementation of observables with async iterators would just be a “pump” and would be very confusing in terms of back-pressure and it would also be a lot slower than a function call. After all the simplest push abstraction is a function call - and observables are in principle just:

class Observable { constructor(subscribe) { this.subscribe = subscribe; } }

It’s a way to defer a function call to later (when you call subscribe) and then that function calls you eagerly.

@TechQuery There actually isn’t a need to call this.refresh with async generator components. When a crank component returns an async iterator object, it will continuously pull values from it until the component is unmounted. This means you could theoretically write code like:

function Component() {
  return new History().listen(this).map((routeData) => (
    <div>{routeData}</div>
  ))[Symbol.asyncIterator]();
}

I dunno if this code would work in your example, this is just to give you an idea of what you could do with async iterators. The two caveats I have are as follows:

  1. you must always yield a value at least once every time you pull props from this, or Crank will throw an error. This means you couldn’t use observable combinators like bufferWhen.
  2. you must return an async iterator not an async iterable. Crank uses duck-typing to look for components which return an object with a next method to determine if that component is a generator/async generator component, because otherwise you wouldn’t be able to return arrays or strings from components without them being exploded so that they yield each element or character per update.

Happy experimenting! If all questions you have are resolved, then you may want to close the issue. If on the other hand, any of this didn’t make sense, ask again and I will try to be more concrete (I’m trying to answer as many issues as I can right now so I’m moving quickly).

Hey, let me know if you need any MobX specific help here - writing an adapter for MobX should be relatively simple although I think reactivity should be first class.

Thanks for messing around with Crank! Unless you’re working with global state, or even in that situation, stateful components should be defined with generator functions, not regular functions. This is because stateless components throw everything away between renders. So when you updated the node variable in your component, it worked the first time, but when the hash was changed, the autorun callback was called in the context of that old execution, and the component started over with a new autorun callback where node was again undefined. If this doesn’t make sense, try putting a console.log statement right before the return of the function returned by observer.

I got your example working here: https://codesandbox.io/s/crank-router-n25wq

One thing I would caution against is calling components directly, as you’ve done in your example. Components can be sync functions, async functions, sync generators or async generators, so it’s not a matter of simply calling the function and getting elements. Hover over the node variable and see its inferred type if you want to see what I’m talking about. You have to handle all these cases if you want to call components directly, which is why I think it’s preferable to let Crank call your components for you. You can do this by not calling the component but creating an element with it.

Let me know if any of this is unclear, Happy experimenting!