12

React Suspense and Error Boundary

 4 years ago
source link: http://neethack.com/2021/01/react-suspense-and-error-boundary/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

React Suspense and Error Boundary

cover image

TLDR: Suspend can catch Promise from children and render fallback until the promise is resolved.

In React 16.6, React is adding the Suspense component that it can render fallback while the app is loading javascript or fetching API. You can see the demonstration from Dan Abramov’s presentation in React conf.

From the documentation on Reactjs webside, the example below:

function ProfilePage({ resource }) { return ( <Suspense fallback={<h1>Loading profile...</h1>}> <ProfileDetails resource={resource} /> <Suspense fallback={<h1>Loading posts...</h1>}> <ProfileTimeline resource={resource} /> </Suspense> </Suspense> ); }

Can render “Loading profile…” while ProfileDetails is loading, and “Loading posts…” while ProfileTimeline is loading. It can control the timing of render components, skip the children while loading, and avoid race conditions in children. However, it doesn’t just work like magic as the document described. Because for the Suspense component to work, the API needs to follow certain criteria.

How Suspense work is similar to the ErrorBoundary in React, for example:

<ErrorBoundary> <ProfileDetails resource={resource}/> </ErrorBoundary>

Can catch any errors thrown in the children and skip the render in children. Suspense is similar to ErrorBoundary, But instead of catching the error, it is catching Promise that is thrown from the children, render fallback while the promise is pending, and unblock the children when the promise is resolved.

To understand how it works, we can take a look at the source code of React.Lazy, React.Lazy can work with Suspense, wrapping javascript import and trigger Suspense fallback while loading the component:

// Create dynamic loading component that trigger Suspense while loading. const LazyComponent = React.lazy(() => import('./Component));

<Suspense fallback={<h1>Loading component</h1>}> <LazyComponent> </Suspense>

A simplified version of React.lazy source code looks like this:

const Unitialized = -1; const Pending = 0; const Resolved = 1; const Rejected = 2;

function lazy(ctor) { // hold component state in closure const payload = { _status: Unitialized, _result: ctor }

return { $$typeof: REACT_LAZY_TYPE, _payload: payload, _init: lazyInitializer } }

function lazyInitalizer(payload) { // import when first initialize if (payload._status === Uninitialized) { const thenable = payload._result(); // Transition to the Pending state. payload._status = Pending, payload._result = thenable

thenable.then( // save result to payload moduleObject => { if (payload._status === Pending) { // Transition to the Resolved state. payload._status = Resolved payload._result = moduleObject.default } }, error => { if (payload._status === Pending) { // Transition to the Rejected state. payload._status = Rejected; payload._result = error; } }, ); } else if (payload._status === Resolved) { // return result when resolved. return payload._result; } else { // throw Promise or Error when status is Pending or Rejected. throw payload._result; } }

Therefore for Suspense to work, the API needs to:

  1. Trigger Promise that loads the data
  2. Throw the Promise while loading
  3. Cache the result and return the result when the Promise is resolved.

Data Fetching

Let’s try to implement data fetching to support Suspense. We can reuse the concept in React.lazy and replace the import with fetch

const Unitialized = -1; const Pending = 0; const Resolved = 1; const Rejected = 2;

function suspenseFetch(url) { const payload = { _status: Unitialized, _result: () => fetch(url) }

return () => { if (payload._status === Uninitialized) { const promise = payload._result() payload._status = Pending payload._result = promise

promise.then((res) => { if (payload._status === Pending) { payload._status = Resolved payload._result = res } }, (err) => { if (payload._status === Pending) { payload._status = Rejected payload._result = err } }) } else if (payload._status === Resolved) { return payload._result } else { throw payload._result } } }

// in app const fetchUser = suspenseFetch(`/users/1`)

const User = () => { const user = fetchUser()

return <div>{user.name}</div> }

const App = () => ( <Suspense fallback={<h1>Loading user...</h1>}> <User> </Suspense> )

With the suspenseFetch function above, we can convert fetch into a suspense compatible API.

use-async-resource is a package that can turns fetch into suspense compatible API too, with support for params and fetching the new result. It is a good resource if you want to implement the API with Suspense.

Conclusion

Suspense is an interesting concept that makes errors and async handling declarative, and it is supported on React level so it will be more stable and easy to handle in the future. However, the Apollo graphql client will not support Suspense API due to the usage of useRef does not support throwing promises and errors. But we will see more libraries in React world support Suspense in the future.

Reference


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK