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

Add a guide on loading data asynchronously #8098

Closed
wants to merge 13 commits into from

Conversation

rthor
Copy link
Contributor

@rthor rthor commented Oct 25, 2016

This guide demonstrates how to load data asynchronously in a React component. I wasn't quite sure where a link to the guide should be placed. Is the Advanced Guides section a good fit?

It addresses the AJAX part in #8060.

@rickbeerendonk
Copy link
Contributor

I like the example and the usage.

In #8060 it also says "mention cancellation issues". Important, because you don't know when fetch returns:

  1. It might be that an old fetch is taking longer and will overwrite the results of a fetch started more recently.
  2. Component is unmounted before the fetch finished.

@rthor
Copy link
Contributor Author

rthor commented Oct 26, 2016

Yeah good point, I forgot to mention that.

Wondering if a Caveat section would suffice since this is not really a problem with React, but rather async execution in general? Fetch, f.ex. has no built-in way of canceling [the promise] unless you wrap it somehow, so I wonder if a code example would be to complex.

Perhaps, fetch is just not the way to go if we want to give an example on cancellation. Other methods might be better at demonstrating how to go about it. Something with cancellation built-in.

@rthor
Copy link
Contributor Author

rthor commented Oct 26, 2016

But if mentioning the issues is enough, then I think a Caveat section is the way to go.

@rickbeerendonk
Copy link
Contributor

Caveat section would be fine with me, don't know what @gaearon thinks about this though.

A standard for canceling fetch is still in process. I suspect your documentation will double in size if you would give an example of a workaround. That would be too much of a distraction.

@rthor
Copy link
Contributor Author

rthor commented Oct 26, 2016

Sending an update tomorrow with the cancellation issues.

@gaearon
Copy link
Collaborator

gaearon commented Oct 27, 2016

This looks like a really great start.

@gaearon gaearon self-assigned this Oct 27, 2016
Copy link
Collaborator

@gaearon gaearon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My comments so far. We'll definitely need to explain the issue with cancellation.


Often, the data that a component needs is not available at initial render. We can load data asynchronously in the `componentDidMount` [lifecycle hook](https://facebook.github.io/react/docs/react-component.html#componentdidmount).

In the following example we use the `fetch` API to retrieve information about Facebook's Gists on GitHub and store them in the state.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetch should link to MDN.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should mention that this is a browser API but it requires a polyfill. Link to the polyfill. Mention that if you use Create React App, polyfill is already included.

---


Often, the data that a component needs is not available at initial render. We can load data asynchronously in the `componentDidMount` [lifecycle hook](https://facebook.github.io/react/docs/react-component.html#componentdidmount).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please leave the domain out of the links. Keep it as /react/docs/react-component.html#componentdidmount.

```javascript{10-14}
class Gists extends React.Component {
constructor(props) {
super(props);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: let's write it shorter,

constructor(props) {
  super(props);
  this.state = {gists: []};
}


render() {
const { gists } = this.state;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: let's not put a newline here.

return (
<div>
<h1>Gists by facebook</h1>
{gists.map(gist => <p><a href={gist.html_url}>{gist.id}</a></p>)}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: let's put the JSX on next line.

{gists.map(gist =>
  <p>...</p>
)}

);
}

/* ... */
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's here? I'd rather avoid ambiguous "old stuff" blocks.

Copy link
Contributor Author

@rthor rthor Oct 27, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just the constructor. Should I repeat it instead? Wasn't sure what your preference would be.

prev: typechecking-with-proptypes.html
---


Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to explain the key concept somewhere early. We need to hammer home the idea that in React, any update means a change in the state. It doesn't have any special capabilities for handling AJAX: if you want to change the UI as a result of data arriving, you need to change the state.

/* ... */
}
```

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have the last section that shows the same with async/await I think. Async/await is enabled in Create React App by default.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, shouldn't all the examples feature async / await? Or is it better to demonstrate both methods?

@@ -32,6 +32,8 @@
title: JSX In Depth
- id: typechecking-with-proptypes
title: Typechecking With PropTypes
- id: loading-data-asynchronously
title: Loading Data Asynchronously
- id: refs-and-the-dom
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we put it here, "refs and DOM" will need its prev link changed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I realized that after I made the PR. Didn't want to change it in case this guide should be elsewhere. Should we keep it here then?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going back to it, I saw that other guides in the same section don't have previous links. I removed the one in this article to keep consistency.

@rthor
Copy link
Contributor Author

rthor commented Oct 27, 2016

Thanks for the feedback! I've gone through everything that you pointed out except for the guide's intro. Working on that ATM.

@rthor
Copy link
Contributor Author

rthor commented Oct 27, 2016

Also, I'll be happy to add async / await. Only concern is that it'll confuse the reader unless all the examples and CodePen are written that way. What are your thoughts on the matter, @gaearon ?

@gaearon
Copy link
Collaborator

gaearon commented Oct 29, 2016

Only concern is that it'll confuse the reader unless all the examples and CodePen are written that way.

I think it'll be fine as a last section with a clear disclaimer we're using something that isn't part of ES2015 yet and needs Babel transform unless you're targeting very modern browsers.


## Pitfalls

An old promise can be pending when a newer promise fulfills. This can cause the old promise to override the results of the new one. If a promise is pending when a component is updated, the pending promise should be cancelled before a new one is created.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically promises can't be "cancelled" so this might read confusing. Maybe "the result of the first promise should be ignored".


An old promise can be pending when a newer promise fulfills. This can cause the old promise to override the results of the new one. If a promise is pending when a component is updated, the pending promise should be cancelled before a new one is created.

Additionally, a component can unmount while a promise is pending. To avoid unexpected behavior and memory leaks when this happens, be sure to also cancel all pending promises in the `componentWillUnmount` [lifecycle hook](/react/docs/react-component.html#componentwillunmount).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, you can't cancel promises. Also it isn't strictly causing memory leaks, just unnecessary requests.
Maybe:

Additionally, a component can unmount while a promise is pending. React warns you if you call setState() on unmounted components to prevent memory leaks. Some data fetching APIs allow you to cancel requests, and this is preferable when component unmounts. For APIs such as fetch() that don't offer a cancellation mechanism, you would need to keep track of whether component is mounted to avoid seeing warnings. Here is how we could implement this:

Then show how to set this.isUnmounted = true; in componentWillUnmount() and change fetch() callback to exit if this flag is set or if current username in props doesn't match the one we were fetching. This would solve both caveats.

As a final example you could then show a more concise way of doing the same with a library that supports cancellation like axios.

@rthor
Copy link
Contributor Author

rthor commented Oct 30, 2016

Sorry for how long it took to address those things. I don't have time to add an example on cancellation using a library right now but I'ld be happy to do it later. If someone else wants to do it, then that's alright too. Otherwise, I think that this is ready.

@rthor rthor mentioned this pull request Nov 1, 2016
13 tasks
@gaearon gaearon assigned lacker and unassigned gaearon Nov 8, 2016
@gaearon
Copy link
Collaborator

gaearon commented Nov 8, 2016

I don't have enough time to finish reviewing this, leaving it to @lacker.
Thank you so much!

3rd-party => third party
put name in link
}

componentDidMount() {
fetch('https://api.github.com/users/facebook/gists')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this code is not using fetch appropriately - you should be checking response.ok to see if there is an error. As is, if there is a 4xx or 5xx error I think it will fail in some weird way during parsing. Even just rejecting a promise would be OK here IMO but fetch actually does not reject the promise on server failure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah sorry, my bad.

We can simplify the `fetchGists` method by using the [`async / await`](https://tc39.github.io/ecmascript-asyncawait/) feature:

```javascript{1,3-4}
async fetchGists() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the best practice is actually to write some helper that uses fetch. Then the nature of this async/await example is different. I think it's still handy to have an example with async/await here though.

Copy link
Contributor Author

@rthor rthor Nov 19, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Helper to handle errors and bad requests then? eg

function safeFetch(url, options) {
  return fetch(url, options).then(response => {
    if (!response.ok) {
      throw new Error(response.statusText)
    }    
    return response.json()
  })
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the async code would be:

async fetchGists() {
  const { username } = this.props;
  try {
    const gists = await safeFetch(`https://api.github.com/users/${username}/gists`);
    this.setState({gists});
  } catch (error) {
    // Request failed...
  }
}

>
> `async / await` is still a proposal for the ECMAScript spec and therefore hasn't been implemented in most browsers. To use it today, a [Babel](http://babeljs.io/docs/plugins/transform-async-to-generator/) (or similar) transform is needed. If you're using Create React App, it works by default.

## Pitfalls
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So basically you could not use any of the previous examples in production because they do not handle the cases where promises are fulfilled out of order, or the component unmounts. Is it even a good idea to explain those paradigms? It does not seem like a great idea to provide sample code that is actually buggy.

Is there a simple way to write code that loads data asynchronously that actually does not have any of these race condition bugs?

Copy link
Contributor Author

@rthor rthor Nov 19, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that I'm aware of, no. Would you prefer that we bake the pitfalls section into each example? Ie provide "bug-free" code from the get-go even if it might be a bit verbose and will probably be explaining to much in one pass for a newcomer.

@lacker
Copy link
Contributor

lacker commented Nov 9, 2016

I tweaked some minor grammatical issues and also provided feedback. Overall I am concerned that if we only provide examples that contain race condition bugs, we are not actually documenting "best practices". We need to promote code that does not have parallelism bugs in it.

Copy link

@hedgerh hedgerh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work so far. I have some ideas for ways to improve the copy. Would you mind if I took a stab at it?

}
```

We can extract the common code in `componentDidMount` and `componentDidUpdate` into a new method, `fetchGists`, and call that in both lifecycle hooks.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should get rid of this refactor step. It adds 40 lines just to explain to the reader that if they write the same code twice, they should probably turn it into a function. The other option is for the first example to already have the fetchGists method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I agree that this adds a lot to the guide, I think that this way is more in tone with the rest of the documentation. Ie show a "simplified" version first and gradually refactor towards best practices.


[Try it out on CodePen.](http://codepen.io/rthor/pen/kkqrQx?editors=0010)

We can simplify the `fetchGists` method by using the [`async / await`](https://tc39.github.io/ecmascript-asyncawait/) feature:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should get rid of all this stuff about async/await, too. It's really out of scope. Our readers are busy learning React, and they either already know about async/await, or shouldn't worry about learning about async/await right here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. But Dan wanted this in the guide.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Async/await is enabled in Create React App by default.

@gaearon: would you mind elaborating on why we should include a section on async/await?

It seems like something better suited for async/await documentation, since it's a pretty trivial example.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like something better suited for async/await documentation, since it's a pretty trivial example.

Not sure what you mean by async/await documentation. Is this something we would write? Which example is trivial?

I think that if we don't show async/await "works" inside components then many people won't realize that. I've seen many surprised reactions to async componentDidMount() because people think it's something React has to "support" as opposed to just being a language feature. So I think it's nice to provide at least a short section giving you enough clues to get started with using it.

}

componentDidUpdate(prevProps) {
// Make sure that the `username` prop did change before
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably only need these comments in the example for the componentDidUpdate example, but no biggie either way.


An old promise can be pending when a newer promise fulfills. This can cause the old promise to override the result of the new one. If a promise is pending when a component is updated, the result of the first promise should be ignored before a new one is created.

Additionally, a component can unmount while a promise is pending. React warns you if you call `setState` on unmounted components to prevent memory leaks. Some data fetching APIs allow you to cancel requests, and this is preferable when a component unmounts. For APIs such as `fetch` that don't offer a cancellation mechanism, you need to keep track of whether the component is mounted to avoid seeing warnings. Here is how we could implement this:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should first mention that the best solution to this issue is to resolve whatever is causing this unmounting issue. API calls should be happening as high up in your component tree as possible. If your top-level component is so far up the tree that you can't make the API call there and pass down the data, that's when I'd start considering keeping my state outside of React.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what's your suggested solution to callback race conditions - just don't unmount a component that's loading data? I don't like having a "pitfalls" section that just states that something is a problem for everyone without showing how to solve it - we should just not have a "pitfalls" part and instead explain "how to do things right".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what you mean by resolving the unmounting issue as it can be a completely correct behaviour and has nothing to do with the request itself.

API calls should be happening as high up in your component tree as possible.

I would argue against this approach. If data is passed down the entire tree, without defining componentShouldUpdate at every step, the entire app will be rerendering with every response.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was imagining some scenario that could be fixed by restructuring your code. For instance, if it'd be possible to move the API call up to the immediate parent, and pass the data down to the component instead. Was I way off base there? :P

Cancellation is obviously the solution when it's unavoidable.

Also, out of curiosity, what are some common use cases where this occurs for people? I understand how it could happen, but I never really encounter it. Route changes was one that came to mind.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Route changes is the easiest case, yes. Go forward, then go back, oops, the wrong thing has arrived.

@rthor
Copy link
Contributor Author

rthor commented Nov 16, 2016

Thanks @lacker for reviewing! I'm quite busy with work at the moment and don't have time to address these issues right now.

@rthor
Copy link
Contributor Author

rthor commented Nov 20, 2016

I think these points are all good, especially the race condition ones. And, I think you're right in that showing buggy code is not optimal. Perhaps I'm going about this the wrong way. The intent of the guide is to address ajax and React but it's turning into a wormhole of JS specific workarounds, not React.

The intro section could be expanded to iterate on all the sections of this guide:

  • Use lifecycle hooks for fetching data.
  • Rerender the app based on fetched data by setting the state.
  • An explanation of how to deal with cancelation.

This all followed by a complete code demo that is not buggy and follows best practices. Short explanations of certain snippets, or code blocks, could be either inlined via comments or explained in more detail at the end of the document.

This demo could then be showcased using the async/await syntax and/or perhaps a third party lib for cancelation.

@brigand
Copy link
Contributor

brigand commented Nov 20, 2016

Just throwing an idea out there, not sure if this is good. It ensures the rest of the chain is just dropped if the component unmounts.

await safeFetch(url)
  .then(...continueIfMounted(this))
  .then((body) => this.setState({body})

// or
const body = await safeFetch(url).then(...continueIfMounted(this))
function continueIfMounted(instance) {
  const originalUnmount = instance.componentWillUnmount
  let isMounted = true
  instance.componentWillUnmount = (...args) => {
    isMounted = false
    if (originalUnmount) {
      return originalUnmount.apply(instance, args)
    }
  }

  return [
    (x) => isMounted ? x : new Promise(() => {}),
    (x) => isMounted ? Promise.reject(x) : new Promise(() => {}),
  ]
}

@lacker
Copy link
Contributor

lacker commented Nov 29, 2016

I think the real problem here is that using React + fetch is actually pretty painful. I didn't realize it before all this discussion happened, but there really is no clean way to avoid the problems that come from race conditions with multiple outstanding fetches, and fetches that return after the component unmounts. So I would no longer recommend that people use React + fetch. The fundamental problem is that you cannot cancel a request - if you can, then the logic is simple - cancel any previous request when you send a new request, or the component unmounts. So I think it would be better for these docs to just use a library that supports cancellation, like axios, and point out these two gotchas so that if people do want to write data loading with fetch they are aware of the gotchas.

@gaearon
Copy link
Collaborator

gaearon commented Nov 29, 2016

Races from multiple fetches while component is mounted are solved the same way unmounting is solved: by cancellation.

@hedgerh
Copy link

hedgerh commented Dec 6, 2016 via email

@gaearon
Copy link
Collaborator

gaearon commented Jan 4, 2017

@rthor @dashtinejad Would either of you be interested working on this but with axios? I'm sorry for the churn here but given the sad status of cancellation in fetch I think we should just make a good axios guide with cancellation and no race conditions.

@dashtinejad
Copy link
Contributor

@gaearon Yes, of course, why not. I'll work on it.

@lacker
Copy link
Contributor

lacker commented Feb 1, 2017

Let's close this one and continue the discussion on #8883

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

Successfully merging this pull request may close these issues.

9 participants