Refactor data fetching with useEffect to Suspense Resources

Kent C. Dodds
InstructorKent C. Dodds

Share this video with your friends

Send Tweet
Published 4 years ago
Updated 3 years ago

A very common practice in React applications today is to request data when a component is rendered within a useEffect callback. Let's refactor that to Suspense with our new resource factory.

Instructor: [00:00] Now, we have a little app where we can choose different Pokémon. We could say Mewtwo, and that will show us Mewtwo's information. These little examples are actually buttons that we can click on, because I don't want to have to type that out every single time.

[00:12] This is currently implemented using useEffect, which is basically what Suspense for data fetching is replacing. Let's go ahead and refactor this to Suspense for data fetching.

[00:21] The first thing that we're going to need is we're going to come down here, and we need to wrap this Pokémon info in a Suspense boundary and error boundary to handle when this component suspends and to handle when there's an error.

[00:32] We already have an error boundary. We just need to import it from our utils here. I'll add error boundary. Then we'll come down here and wrap this in our error boundary. Next, we're going to wrap this in a React Suspense component.

[00:51] We'll specify our fallback to be the Pokémon info fallback, and the name for this fallback is the Pokémon name that the user entered. With this, we're now ready for Pokémon info to suspend. Now, we can scroll up here, and you'll notice we have an error state here, but that's now being handled by our error boundary, so we can get rid of that.

[01:15] We have a pending state, and that's now being handled by the Suspense boundary, so we can get rid of that. All that's left is the success state, so we'll just get rid of this if-statement, because by the time our code gets to here, we should have all the Pokémon all loaded.

[01:28] Actually, we're not going to do any of the loading of the Pokémon directly in this component. Instead, we're going to create a resource when the user selects the Pokémon that they want, and we'll pass that resource to this component.

[01:40] We can get rid of the use effect entirely, get rid of our state management for that asynchronous interaction, and instead of accepting a Pokémon name, we'll accept a Pokémon resource. Then we'll get our Pokémon from the Pokémon resource.read method that we're going to call.

[01:58] We need to manage that state in the component that's rendering our component, so we can pass it along. I'm going to add another use state here for Pokémon resource, and we'll have a set Pokémon resource. We'll initialize that to null.

[02:13] Then instead of rendering this based on the Pokémon name, we'll render it based on the Pokémon resource. Instead of passing the Pokémon name, we'll pass the Pokémon resource to Pokémon info, so it can read that information from the Pokémon resource.

[02:26] Now, when the user submits the form, I'm going to call setPokémonResource, and I'm going to call createResource, which I'm going to need to import. Let's go up here. We'll import that here, and then we're going to use this fetchPokémon method again.

[02:43] I'll call fetchPokémon with the new Pokémon name. This will trigger a re-render with a resource that actually hasn't loaded yet. When the app re-renders, we'll come down here. We say, "Oh, we do have a Pokémon resource, so a Pokémon info will get rendered with that Pokémon resource."

[03:01] When we call Pokémon resource.read, this component will suspend, because that resource data isn't ready. When it suspends, React will catch it and find the nearest React Suspense boundary and render the fallback instead while it's waiting for the Pokémon resource to resolve.

[03:15] When the Pokémon resource resolves, it'll trigger a re-render of the Pokémon info component. This time, when the Pokémon info component calls read, because the resource has resolved, it will have the data that it needs to continue to return the React elements to render the Pokémon information.

[03:32] So, if we save this, and everything was done correctly, then we should be able to click on Pikachu, Charizard, and Mew. In review, basically, what we did here is we took all of the logic that was inside of this Pokémon info component, completely removed it, and replaced it with a prop called Pokémon resource, rather than accepting a prop called Pokémon name.

[03:57] We manage that Pokémon resource in the same place where we manage the Pokémon name. As soon as the Pokémon name changes, we know we need to create a new Pokémon resource. We create that resource using the createResource utility that we built earlier, and then we render our Pokémon info based on the presence of that resource.

[04:15] To make the Pokémon info component suspendable, we had to wrap it with an error boundary and a React Suspense component that has a reasonable fallback for the thing that we're waiting to load. Doing this makes our code way more declarative, which results in a code base that's a lot easier to manage.

[04:30] One last thing I'm going to do here really quick is I'm going to take this createResource call, and I'm going to make a function called createPokémonResource. We're going to just do a little bit of composing here to create a resource specific for a Pokémon.

[04:45] This, I'll pass the new Pokémon name, and then I'll make a function called createPokémonResource. It'll take the Pokémon name, and then it will simply return the same stuff that we just cut. We can clean that up a little bit here.

Darren Seet
Darren Seet
~ 4 years ago

Hi Kent,

Thanks for exposing this unstable API. Most trainers will not put their heads on the chopping block to introduce alpha stage APIs. I was working through the code and it returns a warning message in the console.

Warning: App triggered a user-blocking update that suspended.

The fix is to split the update into multiple parts: a user-blocking update to provide immediate feedback, and another update that triggers the bulk of the changes.

Refer to the documentation for useTransition to learn how to implement this pattern.

I think it might be an unstable API change that came out after you created the lesson. But I just want to some advice on whether this fix I am applying is the correct pattern. Based on what I read in the React docs, I think the fix is to wrap the statements in handleSubmit with the startTransition function.

I added the useTransition hook to the App function

  const [startTransition] = React.useTransition({
    timeoutMs: 1000
  });

then I changed handleSubmit to the following

  function handleSubmit(newPokemonName) {
    // 🐨 set the pokemon resource right here
    startTransition(() => {
      setPokemonName(newPokemonName);
      setPokemonResource(createPokemonResource(newPokemonName));
    });
  }

Initially I only place setPokemonResource inside startTransition and setPokemonName out of it. That seem to work, but if I change the timeoutMs to a low value like 30. It triggers the warning again but it appears that if you wrap setPokemonName in startTransition as well, the the value of timeoutMs no longer triggers the warning no matter what value you set it. Is this the correct approach to do this with the useTransistion hook? Thank you for any help in advance.

Kent C. Dodds
Kent C. Doddsinstructor
~ 4 years ago

Thanks for the detailed message @seetd. Check out lesson #8 😉

Kent C. Dodds
Kent C. Doddsinstructor
~ 4 years ago

Oh, and I'm not 100% certain whether both calls should appear within startTransition or not. I played around with both. Now I think that putting them in startTransition is correct.

Darren Seet
Darren Seet
~ 4 years ago

Thanks. I just saw the lesson plan and I will most likely get to it later in the later. I just I got too far ahead of myself. But I appreciate you taking the time to reply

Viktor Soroka
Viktor Soroka
~ 4 years ago

As I see when the first request fails as a result of the call to nonexistent pokemon and then making a call to an existent one doesn't clear the error result. Only after the click to the try again button the result will be shown. As I understand this is because the error boundary error state is not getting cleared upon the click to the submit button. What is the best way to reset the error boundary state? For now, I made it work via ref with the call errorBoundary.current && errorBoundary.current.tryAgain();.