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

Handling async/event-driven renders? #17

Closed
spiffytech opened this issue Jan 22, 2021 · 9 comments
Closed

Handling async/event-driven renders? #17

spiffytech opened this issue Jan 22, 2021 · 9 comments

Comments

@spiffytech
Copy link
Member

spiffytech commented Jan 22, 2021

How should I handle components that need to rerender when a promise resolves, or when an event fires? I'd figured I could set up my promise/event handler inside mount() and call rerender(), but in the context of #14 I now realize that the args.element provided to mount() will be stale by the time something tries to use it to rerender. The docs show some event handlers, but only ones used in the context of element references that are safe to recreate each render, and not situations where a handler should last the lifetime of the component.

Is there a recommended way to handle this?

@jeswin
Copy link
Member

jeswin commented Jan 22, 2021

You're right that args.element may be stale (if the underlying node changes), but args will not be. So for deferred rendering, you just need to pass args instead of args.element. args.element gets replaced everytime the underlying node changes.

function MailBox() {
  let data = [];

  async function updateData(args) {
    data = await fetchSomething();
    rerender(args.element);
  }

  return {
    render(props, args) {
      updateData(args);

      return data.length === 0 ? (
        <div>Nothing to show.</div>
      ) : (
        <div>Loop through data and display.</div>
      );
    },
  };
}

@jeswin
Copy link
Member

jeswin commented Jan 22, 2021

You could also consider using forgo-state. https://github.com/forgojs/forgo-state

It's about 800 bytes gzipped, and does some interesting optimizations. Like for instance, if a parent and child are bound to the same state, it wouldn't run rerender on the child since the parent will be rerendering anyway.

@spiffytech
Copy link
Member Author

spiffytech commented Jan 22, 2021

I'm using forgo-state for some things, but it doesn't feel right for every task (e.g., requiring an extra library to convert a simple loading flag into Proxy-powered data binding object feels like a complex solution when there's exactly one code point that changes the loading flag), and I wonder if there's a good pattern for handling this built-in, without supplemental libraries.

The code snippet you gave triggers the data update from inside the render() method, which is able to provide updateData with the current args/args.element value. What about when data updates should not be triggered by the render loop? Like this example:

fuction Component() {
  let value = null;
  let es = null;

  return {
    async mount() {
      value = await fetchInitialValue();
      rerender(???);

      es = new EventSource(`/events?since=${value.id}`);
      es.onmessage = (event) => {
        value = event.data;
        rerender(???);
      }
    },

    render() {
      return <p>{value}</p>;
    },

    unmount() {
      es?.close();
    }
  }
}

I tried basically this, passing mount()'s args.element to rerender() and got the same double-render behavior as from #14. I solved it with forgo-state, and I suppose I could solve it by having render() assign args to a variable in closure scope where mount() would access it to use with rerender(), but I'd like to understand if there's a better way to handle it.

@spiffytech
Copy link
Member Author

Does it make any sense for Forgo to assign element as a property of the component object? So anything in the component could do rerender(this.element) without worrying about holding a stale reference?

@jeswin
Copy link
Member

jeswin commented Jan 22, 2021

Actually, I'd expect the code above to work - provided you've defined mount() as mount(props, args).
If not, it's a bug. Can you post the actual code please?

@jeswin
Copy link
Member

jeswin commented Jan 22, 2021

Does it make any sense for Forgo to assign element as a property of the component object? So anything in the component could do rerender(this.element) without worrying about holding a stale reference?

I did consider it, but I wanted to avoid the this pointer.

@jeswin
Copy link
Member

jeswin commented Jan 22, 2021

I've made a codesandbox link for you here: https://codesandbox.io/s/dank-pond-3dgxk

You could have done it from render() as well, instead of mount.

@spiffytech
Copy link
Member Author

Can you post the actual code please?

I can no longer reproduce with the latest forgo+forgo-state+forgo-router, though I could from the combination of those I had after capturing the #14 fixes. The rerender-from-mount() pattern is working in the sandbox you linked, and now in my project too.

Thanks for the input. I'll submit a PR for the README documenting async logic soon-ish.

@jeswin
Copy link
Member

jeswin commented Jan 22, 2021

Thank you.

It could have been that forgo-state and forgo-router were referencing older forgo versions. I expect all of them to stabilize this week. Fragment support is the big outstanding ticket, coming soon.

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