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.
Thanks for the detailed message @seetd. Check out lesson #8 😉
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.
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
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();
.
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.
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 theApp
functionthen I changed
handleSubmit
to the followingInitially I only place
setPokemonResource
insidestartTransition
andsetPokemonName
out of it. That seem to work, but if I change thetimeoutMs
to a low value like30
. It triggers the warning again but it appears that if you wrapsetPokemonName
instartTransition
as well, the the value oftimeoutMs
no longer triggers the warning no matter what value you set it. Is this the correct approach to do this with theuseTransistion
hook? Thank you for any help in advance.