Home >Web Front-end >CSS Tutorial >React Suspense: Lessons Learned While Loading Data
React Suspense: Lessons learned from data loading
Suspense is a upcoming feature from React that helps coordinate asynchronous operations such as data loading, making it easy for you to prevent state inconsistencies in the UI. I'll explain in more detail what exactly this means, and give a brief introduction to Suspense, then a more realistic use case and some lessons learned.
The functions I introduced are still in the alpha stage and must not be used in production environments. This post is for those who want to get the first look at the upcoming features and understand where they will be heading into the future.
One of the most challenging parts of application development is coordinating application state and how data is loaded. State changes usually trigger new data loading in multiple locations. Typically, each piece of data will have its own loading UI (such as a "rotator") that is roughly where the data is in the application. The asynchronous nature of data loading means that these requests can be returned in any order. As a result, not only will your application appear and disappear with many different spinners, but worse, your application may display inconsistent data. If two of your three data loads are done, you will see a load spinner at the top of the third position, still showing the old, now outdated data.
I know that's too much. If you find any of it confusing, you may be interested in a previous article about Suspense that I wrote. This article introduces in more detail what Suspense is and what it implements. Note that some of these small details are now outdated, i.e. the useTransition
hook no longer accepts timeoutMs
values, but waits indefinitely.
Now, let's take a quick look at the details and then move on to a specific use case with some lurking traps.
Fortunately, the React team is smart enough to not limit these efforts to just loading data. Suspense works through low-level primitives, which you can apply to almost anything. Let's take a quick look at these primitives.
First of all<suspense></suspense>
Boundary, it accepts a fallback
attribute:
<suspense fallback="{<Fallback"></suspense> }>
Whenever any child components under this component hang, it renders fallback
. No matter how many subcomponents are suspended, for whatever reason, fallback
is displayed. This is one way React ensures that the UI is consistent – it won't render anything until everything is ready.
But what happens when the user changes the status and loads new data after the content is initially rendered? We certainly don't want our existing UI to disappear and show our fallback
; that would be a bad user experience. Instead, we might want to display a loading spinner until all the data is ready before the new UI is displayed.
The useTransition
hook implements this. This hook returns a function and a boolean value. We call the function and wrap our state changes. Now things are getting interesting. React tries to apply our status changes. If anything hangs, React sets the boolean value to true
and waits for the hang to end. Once done, it will try to apply the status change again. Maybe it will succeed this time, or maybe something else will hang. Anyway, the Boolean flag will remain true
until everything is ready, and only then will the state change be completed and reflected in the UI.
Finally, how do we hang? We hang by throwing a promise. If the data is requested and we need to fetch, then we fetch – and throw a promise related to that fetch. This low-level suspend mechanism means we can use it for anything. The React.lazy
utility for lazy loading components already works with Suspense, and I've written before using Suspense to wait for the image to load before displaying the UI to prevent content from moving.
Don't worry, we'll discuss all of this.
We will build something slightly different from the examples in other similar articles. Remember that Suspense is still in the alpha phase, so your favorite loading data utility may not have Suspense support yet. But that doesn't mean we can't fake something and understand how Suspense works.
Let's build an infinite loading list that displays some data and combines some preloaded images based on Suspense. We will display our data, as well as a button to load more data. When the data is rendered, we will preload the associated image and hang before it is ready.
This use case is based on the actual work I did in my side project (again, don't use Suspense in production-but the side project is allowed). I was using my own GraphQL client at the time and the motivation for this post was some of the difficulties I had. To simplify operations and focus on Suspense itself, rather than any single data loading utility, we will just forge data loading.
This is the sandbox we initially tried. We will use it to explain everything step by step, so there is no need to rush to understand all the code now.
Our root App
component renders a Suspense boundary like this:
<suspense fallback="{<Fallback"></suspense> }>
fallback
renders whenever anything hangs (unless the state change occurs in the useTransition
call). To make things easier to understand, I made this Fallback
component turn the entire UI pink, so it's hard to miss; our goal is to understand Suspense, rather than building a high-quality UI.
We are loading the current data block inside DataList
component:
const newData = useQuery(param);
Our useQuery
hook is hardcoded to return fake data, including a timeout for simulated network requests. It handles cached results and throws a promise if the data is not cached.
We save the status (at least for now) in the main data list we are displaying:
const [data, setData] = useState([]);
When new data is passed in from our hook, we append it to our main list:
useEffect(() => { setData((d) => d.concat(newData)); }, [newData]);
Finally, when the user needs more data, they click the button, which calls this function:
function loadMore() { startTransition(() => { setParam((x) => x 1); }); }
Finally, note that I'm using the SuspenseImg
component to handle the preload of the image I'm displaying with each piece of data. Only five random images are displayed, but I added a query string to make sure new loads are made for each new piece of data we encounter.
To summarize where we are currently at, we have a hook that loads the current data. This hook follows the Suspense mechanism and throws a promise when loading occurs. Whenever that data changes, the total list of running projects is updated and new projects are attached. This happens in useEffect
. Each project renders an image, we use the SuspenseImg
component to preload the image and hang before it is ready. If you are curious about how some code works, check out my previous post on preloading images with Suspense.
If everything works fine, this will be a very boring blog post, don't worry, it's not normal. Note that the pink fallback
screen will be displayed and then quickly hidden at initial load, but will then re-display.
When we click the button to load more data, we see the inline load indicator (controlled by useTransition
hook) flip to true
. Then we see it flip to false
, and our original pink fallback
displays. We expect that pink screen no longer sees after the initial load; the inline load indicator should be displayed until everything is ready. what happened?
It has been hidden in a conspicuous place:
useEffect(() => { setData((d) => d.concat(newData)); }, [newData]);
useEffect
runs when the state change is completed, i.e. the state change has been hanged and applied to the DOM. That part, "Completed Pending" is the key here. We can set the state here if we want, but if that state change hangs again, it's a brand new hang. This is why we see pink flashing after the initial load and subsequent data loading. In both cases, the data loading is done, and we then set the state in one effect, which causes the new data to actually render and hang again because the image is preloaded.
So, how do we solve this problem? On one level, the solution is simple: stop setting the state in the effect. But this is easier said than done. How do we update the list of running entries to attach new results without using effects? You might think we can use ref to track things.
Unfortunately, Suspense brings some new rules about refs, that is, we cannot set refs inside the render. If you're wondering why, remember that Suspense is all about React trying to run the render, seeing the promise being thrown, and then discarding that render halfway through. If we change the ref before the rendering is cancelled and discarded, the ref still has that change, but has an invalid value. The rendering function needs to be pure and has no side effects. This has always been a rule for React, but now it's more important.
This is the solution, we will explain it paragraph by paragraph.
First, instead of storing our main data list in state, do something different: let's store the list of pages we're viewing. We can store the most recent page in the ref (although we won't write it in the render) and store an array of all currently loaded pages in state.
const currentPage = useRef(0); const [pages, setPages] = useState([currentPage.current]);
To load more data, we will update accordingly:
function loadMore() { startTransition(() => { currentPage.current = currentPage.current 1; setPages((pages) => pages.concat(currentPage.current)); }); }
However, the tricky part is how to convert these page numbers into actual data. What we can't do is loop through these pages and call our useQuery
hook; the hook cannot be called in the loop. We need a new, non-hook-based data API. According to the very unofficial convention I've seen in my past Suspense demos, I named this method read()
. It won't be a hook. If the data is cached, it returns the requested data, otherwise a promise will be thrown. For our fake data loading hook, there was no need to make any real changes; I simply copied and pasted the hook and renamed it. But for the actual data loading utility library, it may take some work for the author to expose both options as part of their public API. In the GraphQL client I mentioned earlier, there is indeed both the useSuspenseQuery
hook and read()
method on the client object.
With this new read()
method, the last part of our code is trivial:
const data = pages.flatMap((page) => read(page));
We take each page and request the corresponding data using our read()
method. If any page is not cached (it should actually be only the last page in the list), a promise will be thrown and React will hang for us. When promise parses, React will try the previous state change again, and this code will run again.
Don't let flatMap
call confuse you. It's exactly the same thing as what map
does, except it gets every result in the new array and "flatten" it if it's itself an array.
With these changes, when we start, everything works as expected. Our pink loading screen is displayed once on initial load, and then in subsequent loading, the inline loading status is displayed until everything is ready.
Suspense is an exciting update to React. It's still in the alpha phase, so don't try to use it for anything important. But if you are the kind of developer who likes to get the first to experience upcoming content, then I hope this post provides you with some useful background and information that will be useful when published.
The above is the detailed content of React Suspense: Lessons Learned While Loading Data. For more information, please follow other related articles on the PHP Chinese website!