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

Support for asynchronous values (like Promises and Observables) #6481

Closed
ghost opened this issue Apr 11, 2016 · 14 comments
Closed

Support for asynchronous values (like Promises and Observables) #6481

ghost opened this issue Apr 11, 2016 · 14 comments

Comments

@ghost
Copy link

ghost commented Apr 11, 2016

At the moment, it is quite cumbersome to work with promises in React. Either you write a lot of code to update the state inside the then() callbacks or you use a library like react-promise which only works for children (not properties) because it is based on components.

Another option is to traverse the whole virtual dom and replace promises before they are passed to React. (I have read once a blog post about this but cannot find it any longer.) This is obviously not ideal for the rendering performance.

Since promises are now a standard, I think there should be simpler way to use them directly in the view. One option would be to introduce an AsyncValue type with a Promise implementation. The AsyncValue type would then be supported for children as well as properties.

Promises could then simply be used like this:

<div>{promise(this.state.myPromise)}</div>

Opposed to supporting promises everywhere (directly without the promise function), this should not have unexpected side effects. Moreover, other types of asynchronous values (for example Observables) could be added later easily just by creating another AsyncType implementation.

Angular 2 takes a similar route with the async pipe. There you can simply write

{{ myPromise | async }}

in attributes as well as normal content.

This feature would also be very convinient for libraries like Flux because it allows to fetch properties directly inside the view and avoids the reptition which you have with Relay:

<div>{promise(this.state.model.get('property'))}</div>

What do you think about this proposal?
Has this been discussed before? I couldn't find a thread which covers this particular topic.

@iamdustan
Copy link
Contributor

Hey @joelrich,

I’m fairly certain that this proposal runs counter to some of the goals the project has and the direction it is headed. Specifically around scheduling.

While not a direct answer, you can glean some of the thoughts surrounding this in the following threads:

Unable to find a source, default react state is natively serializeable and synchronous right now. This allows React to reason about and have tight control over reconciliation and rendering. This also allows React to decide on scheduling. Promises as an abstraction remove all of these properties making current and future optimizations difficult or impossible.

@ghost
Copy link
Author

ghost commented Apr 13, 2016

Hi @iamdustan,

Do you see another way to improve the support for promises - especially for use cases with libraries like Falcor where most values in the view are promises? Or do you think the current solution is good enough?

I do not think that the scheduling is a big problem. Similar to then + setState, you would only have to schedule an update if the promise is resolved/rejected.

What exactly do you mean with "react state is natively serializeable"? For example, if you put an object of an ES6 class into the state, is the state then still serializable? Sure, you can JSON.stringify it, but you lose the information about the class. You would not be able to parse it and replace the previous state with it without risking that there will be problems. Is there a reason why the view state has to be serializable?

@zpao
Copy link
Member

zpao commented Apr 13, 2016

cc @sebmarkbage for some additional thoughts / links.

@nfcampos
Copy link

don't know if this helps but see https://github.com/heroku/react-refetch (specifically https://github.com/heroku/react-refetch/blob/master/src/PromiseState.js) for a serialisable, immutable representation of the state of a Promise that you can use to stand in for a promise in a way that makes sense inside a render method.
cc @ryanbrainard

@sebmarkbage
Copy link
Collaborator

This has been discussed so many times that there is no single easy link to post to. We should probably make a new summary but I'll do my best here.

We did have an idea to allow promises passed to setState(promise) as a convenience. That would also allow it to clean up itself automatically if the component gets unmounted if it gets resolved earlier.

  1. If you allow a Promise or Observable in render, you end up in an intermediate state. What do you do with your UI in the mean time? What goes into the <div> in the example of <div>{promise(this.state.myPromise)}</div> before it gets resolved? What about when the value updates? We didn't want to encourage a system where you could block the UI by accidentally putting a too long Promise in there.

  2. One of the benefits of React over what came before it was that as a user you never deal with specialized data structures. Like change tracking sets, Promises or Observable values. It's all plain data. async/await gets closer to that but still requires a lot of explicit opt-in. In other languages there are solutions that doesn't require this but still have the capabilities that you want here.

  3. Promise and Observable chains execute whenever the async dependencies get resolved. They're push oriented. When something happens, immediately compute all the artifacts of it. The scheduling work mentioned above is about making sure that such operations don't steal CPU cycles. Because if that work is low-priority, it will cause responsiveness issues for higher priority work.

Now there are mitigating factors for all these things:

  1. We have an idea for how we could do forked behavior of components where they could live in two states at once and then rebase. This would allow a component to update itself (remain responsive) while a pending update is still being processed.

  2. We could potentially design the API around generators or async/await to avoid exposing users to first-class Promise values. (Or even better, algebraic effects.)

  3. The ECMAScript Zones proposal could allow for overriding the default scheduling of Promises. Allowing us to defer work that is not high priority. This would make the whole idea of push oriented values more palatable.

@ghost
Copy link
Author

ghost commented Apr 14, 2016

@sebmarkbage, thank you for the summary. My take on 1) is that the promise function would allow to specify a loading value. Example:

<div>{promise(this.state.myPromise, 'Loading...')}</div>

Alternatively, you could also wrap multiple elements with another helper function to wait until all promises are resolved. Since this function would just return another promise, this would not have to be part of React as long as it is allowed to return a (wrapped) promise from render().

Regarding 3), I am still not sure if I understand the problem. When the promise is resolved, the behaviour could be very similar to promise.then(v => this.setState({value: v})). I haven't looked at how setState is implemented, but if for the example the DOM update only happens in the next animation frame, then it would still be possible to do the same: If the promise is resolved, schedule a DOM update for this virtual DOM subtree and perform it (together with other promise changes) during the next animation frame.

@satya164
Copy link

@joecritch How about you have a component which can render a promise? Something like,

<PromisedValue
    promise={promise}
    renderLoading={() => <...>}
    renderItem={value => <...>}
    renderError={err => <...>}
/>

Should be easy enough to write such a component.

@ghost
Copy link
Author

ghost commented Apr 14, 2016

@satya164 unfortunately, this does not work for properties.

@pie6k
Copy link

pie6k commented Oct 6, 2017

Do you think API like this could make any sense?

class UserData extends Component {
  *render() {
    yield <div>
      <h2>Good morning</h2>
      {
        yield <div>Loading...</div>;
        const userData = yield whenDidMount(fetchUser(this.props.userId));
        yield <div>Hello, {userData.username}</div> 
      }
    </div>
  }
}

I imagine it might be a bit chaotic, but main idea here is that render function is generator and it is able to yield 'next states' of components that would replace each other.

Maybe it could be 'next level' of declarative nature of React. Right now you are able to be declarative when describing how UI looks basing on current data - with such approach you could be able to be declarative when describing everything that can happen during component lifetime.

More complex scenarios could look like:

class OpenPropmptAndWait extends Component {
  *render() {
    yield <div>
      {while(true) {
        yield <div onClick={/* ??? not sure. Maybe this.nextStep(); ?? */}>click to show prompt popup</div>
        yield this.onNextStep();
        yield <div>
          Are you sure to do ABC?
          <div onClick={/* this.nextStep('yes') ???  */}>Yes</div>
          <div onClick={/* this.nextStep('no') ???  */}>No</div>
        </div>
        const promptResult = yield this.onNextStep();
        yield <div>You clicked {promptResult}. You will see prompt open button again in 2 seconds</div>
        yield wait(2000);
      }}
    </div>
  }
}

In such component, it's really obvious not only how component will look at any state, but also what is next possible thing that could happen.

In current version of React, you only see what are possible scenarios, not what are possible sequences of them.

@antanas-arvasevicius
Copy link

antanas-arvasevicius commented Oct 6, 2017

omg, no sense in a today world. better would be to use async:

class UserData extends Component {
  render() {
    return
    <div>
      <h2>Good morning</h2>
       renderAsync(
           async () => {
              const userData = await whenDidMount(fetchUser(this.props.userId));
              return <div>Hello, {userData.username}</div> 
           },
           () =><div>Loading...</div>,
           (e) =><div>error</div> 
      )
    </div>
  }
}

@thysultan
Copy link

thysultan commented Oct 6, 2017

The heuristic i settled on for handling transient states in DIO was to allow Promises as both render return types & element types, and additionally allow a Promise component to describe children that render before it resolves. For example.

const Something = Promise.resolve(<ul><li>Something</li></ul>)

render(
<Something>
    <h1>Loading</h1>
    <span>...</span>
</Something>
)

@gaearon
Copy link
Collaborator

gaearon commented Apr 8, 2018

Second part of my talk (about “suspense”) is relevant to this: https://reactjs.org/blog/2018/03/01/sneak-peek-beyond-react-16.html

@gaearon
Copy link
Collaborator

gaearon commented May 14, 2018

I’m going to close this because Suspense API is designed exactly for this purpose, and it has been merged into master. It’s not going to be available or usable until later this year but there’s no need to keep this issue open if we have an initial solution in the codebase and will keep iterating on it.

When it’s fully ready you’ll read it on the React blog.

#12279

@gaearon
Copy link
Collaborator

gaearon commented Apr 14, 2023

Just to follow up on this.

If you allow a Promise or Observable in render, you end up in an intermediate state. What do you do with your UI in the mean time? What goes into the <div> in the example of <div>{promise(this.state.myPromise)}</div> before it gets resolved? What about when the value updates? We didn't want to encourage a system where you could block the UI by accidentally putting a too long Promise in there.

As noted earlier, we solved this with <Suspense> that lets you specify declarative placeholders anywhere in the tree:
https://react.dev/reference/react/Suspense#usage

We have an idea for how we could do forked behavior of components where they could live in two states at once and then rebase. This would allow a component to update itself (remain responsive) while a pending update is still being processed.

We've implemented this in React 18 (using "concurrent rendering"). For example, useTransition lets you start rendering new state "in background" while previous state remains interactive on the screen: https://react.dev/reference/react/useTransition#usage

As for the original request, reactjs/rfcs#229 addresses it. It just took us six years to figure out how to do it right.

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

No branches or pull requests

9 participants