The article was translated from source: https://hackernoon.com/concurrent-react-using-suspense-and-usetransition-to-build-better-ux-cman2cdd
The React JS
developer team announced some interesting changes a few months ago – React will receive Concurrent Mode
. Basically, this will allow React
to implement multiple user interfaces simultaneously. Of course, JavaScript
is a single thread and at the same time really an illusion
, but the new features will allow web applications (and native applications once they use React Native) to respond quickly and flexibly. than it does with less effort and custom code from the developer to accomplish this.
Concurrent Mode
currently available in React
‘s test build, so learn and see how to use the new API.
In this article I will show you how to use the Suspense API
with the useTransition
hook
. Another type of hook
, useDeferredValue
, serves a slightly different, but equally important purpose.
But what about the existing Suspense feature?
You will notice that the API Suspense
has been available in React
since version 16.6
. In fact, this is the same API
being expanded to do more in React
test builds. In React 16.6
, Suspense
can only be used for one purpose: to split code
and load components lazy loading
using React.lazy ()
.
The New Way – Render as you fetch
This has been discussed a lot through conversations and blogs and in the official documentation, so I’ll talk briefly – Concurrent React
allows us to deploy render as you fetch
, which renders component
as data. The data needed to fill them is fetched simultaneously. React renders
as much as possible without the data available, and the renders component
requires the data to be fetched as soon as the data is available. During this time, these components were said to be suspended
.
The Setup
For this, I’m using the basic React app
that I’ve manually configured with Webpack
and Babel
, with the only difference running:
1 2 | npm i <a class="__cf_email__" href="/cdn-cgi/l/email-protection">[email protected]</a> ntal <a class="__cf_email__" href="/cdn-cgi/l/email-protection">[email protected]</a> --save |
To get test versions instead of installing release versions
of react
and react-dom
.
Opting in to Concurrent Mode
Because Concurrent Mode
changes how React
handles components essentially, you will need to change the ReactDOM.render()
line in your index.js
to:
1 2 | ReactDOM.createRoot(document.getElementById('root')).render(<App />); |
This enables concurrency mode in your application.
I also set up App.js
to display a component called Data
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import React from 'react'; import Data from './Data'; const App = () => { return ( <div> <p>React Concurrent Mode testing</p> <Data /> </div> ); } export default App; |
The Demo
Now create Data.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | import React, { useState, useTransition, Suspense } from 'react'; import DataDisplay from './DataDisplay'; import { dataFetcher } from './api'; const initialData = { read: () => { return { foo: "initial" } } }; const Data = () => { const [data, setData] = useState(initialData); const [count, setCount] = useState(0); const [startDataTransition, isDataPending] = useTransition({ timeoutMs: 2000 }); const fetchNewData = () => { startDataTransition(() => { setData(dataFetcher()) }) } return ( <div> <Suspense fallback={<p>Loading...</p>}> <DataDisplay data={data} /> <button disabled={isDataPending} onClick={() => fetchNewData()}>Click me to begin data fetch</button> </Suspense> <p>Counter: {count}</p> <button onClick={() => { setCount(count + 1); }}> Click me to check if the app is still responsive</button> </div> ) } export default Data; |
dataFetcher
is a special object
return function that lets React
know the states
because this object
can be fetched
when components
depending on this state
are rendered
. These components
suspends
if the data has not been fetched yet. We will look at how to create special object
.
initData
displays the format of the object
returned by the dataFetcher when the data has finished loading. It has the function to read and return the object with the data we need. Ideally initData
should implement some sort of storage function for the data that was last loaded, but here we only use {foo: initial}
.
A state
that while update/fetched
causes a component to hang, must be updated using the hook useTransition
. This Hook
returns a pair of values - a function with a callback function in which you set the status and a boolean
let us know when the transition is taking place.
The argument passed to useTransition
is an object that tells React
how long to wait before pausing the component. To understand it, think of it this way: We have some data on the screen and we are fetching some new data to replace it. We want to show the spinner
while new data is being fetched, but users can see the old data for a second or maybe half a second before the spinner
is displayed. This delay is mentioned in this object.
This is useful in cases when displaying old data until new data is loaded as desired and also to prevent spinner
showing for a fraction of a second (causing jitter-like errors) during fetch operations. fast data.
Let’s take a closer look at Suspense
:
1 2 3 4 5 | <Suspense fallback={<p>Loading...</p>}> <DataDisplay data={data} /> <button disabled={isDataPending} onClick={() => fetchNewData()}>Click me to begin data fetch</button> </Suspense> |
Any component that will be suspended is wrapped inside Suspense component
. Inside the fallback props
, we switch the component
will be displayed instead while the inner component
is waiting for data. This is usually a spinner
or loading indicator
of some sort to visually show the user what is happening, so it doesn’t appear as if the page is unresponsive to clicks.
Here I used boolean isDataPending
to disable the button while the data was being fetched, preventing the user from pressing the button multiple times and sending multiple requests.
All JavaScript
components in the page continue to operate while the component is hanging and data is being fetched. The counter to increase it can be used to confirm this.
DataDisplay
is a simple component that retrieves data and invokes its read function and displays the results.
1 2 3 4 5 6 7 8 9 10 | import React, { memo } from 'react'; const DataDisplay = ({ data }) => { return ( <h3>{data.read().foo}</h3> ) } export default memo(DataDisplay); |
Finally, we look at DataFetcher
and other things inside api.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | export const dataFetcher = (params) => { return wrapPromise(fetchData(params)) } const wrapPromise = (promise) => { let status = "pending"; let result; let suspender = promise.then( r => { status = "success"; result = r; }, e => { status = "error"; result = e; } ); return { read() { if (status === "pending") { throw suspender; } else if (status === "error") { throw result; } else if (status === "success") { return result; } } }; } const fetchData = (params) => { // In a real situation, use params to fetch the data required. return new Promise(resolve => { setTimeout(() => { resolve({ foo: 'bar' }) }, 3000); }); } |
wrapPromise
is responsible for everything integrated with React
and should be simple if you’ve used Promise
before. It returns the result if the fetching process is successful, raise
error if any, and throw the Promise
with a “pending” status if the operation has not been completed.
Thanks and hope the article is useful in your work