On October 24, 2019, React officially released the first early community preview document on Concurrent mode on the first day of the React Conf 2019. It’s exciting to officially meet the React public.

Like last year’s React Hooks, although Concurrent is experimental, I’m sure it won’t take long…


I’ve been holding on to this for over four years


If React Hooks are designed to improve the development experience, Concurrent mode is focused on improving the user experience. On the face of it, it may not have much impact on our development. React has changed a lot internally.

This series of articles is mainly from the official preview documentation for React early adopters.

This month the Vue 3.0 source code was released and the nuggets articles exploded. There’s no reason to React so ‘big news ‘(even though it’s been known for 3 years)… I’ll take the lead.


🎉 next:React Concurrent mode previews the next part: useTransition’s parallel world


Article Content framework

  • What is Concurrent mode?
  • Enabling Concurrent Mode
  • What is a Suspense?
  • The implementation principles of Suspense
  • Cache the asynchronous operation state of Suspense
    • Use the Context API
    • Extract the cache state to the parent level
  • Concurrent initiation of requests
  • To deal with race
  • Error handling
  • Suspense choreography
  • conclusion
  • The resources




What is Concurrent mode?

This is a set of features that will keep your React app responsive and can be adjusted gracefully according to the user’s device capabilities and network conditions. This feature set, which contains optimizations in two directions:

1️ CPU-intensive (CPU-bound)

CPU intensive refers to the optimization of Reconcilation(or Diff). Under the Concurrent mode, Reconcilation can be interrupted to make way for higher-priority tasks that keep applications responsive.

Last week, I posted an article ahead of React Conf 2019🔥 This is probably the most popular way to open React Fiber.🤓, if you want to learn more about Concurrent mode, it’s highly recommended to start with this article!

The CPU-intensive optimization remained compatible with existing code, exposed little new API, and the main effect was to scrap some of the lifecycle methods, which are well known.


2️ I/ O-intensive (I/ o-bound)

React is optimized for handling asynchrony. The main weapons are Suspense and useTransition:

  • Suspense– New asynchronous data processing mode.
  • useTransition– Provides a pre-render mechanism, React can be in ‘another branch’pre-rendered, wait for the data to arrive, and then render it all at once, reducing the intermediate display of loaded state and page jitter/flicker.


I won’t go into depth in explaining what the Concurrent pattern is in this article; Suspense will be covered in this one, and useTranstion will be covered in the next one.




Enabling Concurrent Mode

The Concurrent mode is currently experimental, and you can install the experimental version by running the following command:

npm install react@experimental react-dom@experimental
# or
yarn add react@experimental react-dom@experimental
Copy the code

As mentioned above, this is for early adopters, although the API should not change much and should not be used in a production environment.


Start Concurrent mode:

import ReactDOM from 'react-dom';

ReactDOM.createRoot(
  document.getElementById('root')
).render(<App />);
Copy the code


Another thing to note is that when Concurrent mode is enabled, previously deprecated lifecycle methods become completely unusable. Make sure your code has migrated.




What is a Suspense?

Suspense should be familiar to everyone; it’s already available in V16.5, but it’s often used in conjunction with React. Lazy to implement code separation:

const OtherComponent = React.lazy((a)= > import('./OtherComponent'));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>} ><OtherComponent />
      </Suspense>
    </div>
  );
}
Copy the code


React. Lazy this is a small attempt, but it can be useful in Suspense.

Suspense in Chinese means to wait, hang, or hover. React offers a more canonical definition:

Suspense is not a ‘data fetching library’ but rather a mechanism for the ‘data fetching library’ to tell React that the data is not ready, and React will wait for it to be finished before continuing to update the UI. Suspense is a mechanism for asynchronous processing provided by React, not a specific data request library. It is a native component asynchronous call primitive provided by React. It is an important role in the Concurrent schema feature set.


You can now use Suspense much cooler, and trust me, it will be a sword in your hand in no time. With it you can request remote data like this:

function Posts() {
  const posts = useQuery(GET_MY_POSTS)

  return (<div className="posts">
    {posts.map(i => <Post key={i.id} value={i}/>)}
  </div>)
}

function App() {
  return (<div className="app">
    <Suspense fallback={<Loader>Posts Loading...</Loader>} ><Posts />
    </Suspense>
  </div>)}Copy the code


Load dependency scripts:

function MyMap() {
  useImportScripts('//api.map.baidu.com/api?v=2.0&ak= your key ')

  return (<BDMap />)
}

function App() {
  return (<div className="app">
    <Suspense fallback={<Loader>Map loading...</Loader>} ><MyMap />
    </Suspense>
  </div>)}Copy the code


Looking closely at the code above, it has two features:

  • 1️ We needSuspenseTo wrap these components that contain asynchronous operations and provide them withThe fallback (fallback). This rollback is displayed during an asynchronous request.
  • 2️ The code above acquires asynchronous resources just as if it were a synchronous call. You’re right. With Suspense, we can matchasync/awaitorGeneratorAgain, use the ‘synchronous’ code style to handle asynchronous requests


Amazing, right? How does React do it?




The implementation principles of Suspense

The implementation of Suspense has been analyzed in the past. It uses a mechanism similar to React’s ErrorBoundary to implement it, which is very imaginative.

🤔 uh… If the ErrorBoundary is used, it can be used to catch the exception of the lower component. When we do the asynchronous operation, we can throw an exception to interrupt the rendering. When we finish the asynchronous operation, we can tell React that we are ready, please continue rendering…

🤭 This explains why async/await and Generator are not used, but asynchronous operations can be handled in a synchronous style. Throw can interrupt code execution…

🤔 But this’ exception ‘should be different from a normal exception and it should also notify the ErrorBoundary that the asynchronous operation is ready, allowing it to continue rendering the child node…


I think the process goes like this:


In fact, the best ‘exception object’ available for this scenario is a Promise. Roll up your sleeves and implement one:

export interface SuspenseProps {
  fallback: React.ReactNode
}

interface SuspenseState {
  pending: boolean error? : any }export default class Suspense extends React.Component<SuspenseProps.SuspenseState> {
  // ⚛️ First, record whether we are in the mounted state, because we don't know when the asynchronous operation is finished, perhaps after unmounting
  // setState cannot be called after the component is uninstalled
  private mounted = false

  // Component status
  public state: SuspenseState = {
    // ⚛️ indicates that you are blocking on an asynchronous operation
    pending: false.⚛️ indicates that the asynchronous operation has a problem
    error: undefined
  }

  public componentDidMount() {
    this.mounted = true
  }

  public componentWillUnmount() {
    this.mounted = false
  }

  // ⚛️ Uses the Error Boundary mechanism to catch lower-level exceptions
  public componentDidCatch(err: any) {
    if (!this.mounted) {
      return
    }

    // ⚛️ Determine if it is a Promise, if not, throw up
    if (isPromise(err)) {
      // Set the pending state
      this.setState({ pending: true })
      err.then((a)= > {
        // ⚛️ Asynchronous execution succeeds. The pending state is closed and re-rendering is triggered
        this.setState({ pending: false })
      }).catch(err= > {
        // ⚛️ Asynchronous execution failed. We need to handle this exception properly and throw it to React
        / / because in an asynchronous callback, throw an exception here cannot be captured in the React, so we first recorded here
        this.setState({ error: err || new Error('Suspense Error')})})}else {
      throw err
    }
  }

  // ⚛️ throws an exception to React here
  public componentDidUpdate() {
    if (this.state.pending && this.state.error) {
      throw this.state.error
    }
  }

  public render() {
    // ⚛️ Render fallback while pending
    return this.state.pending ? this.props.fallback : this.props.children
  }
}
Copy the code


⚠️ Note that the code above is only available before V16.6 (not included). With the release of Suspense 16.6, Suspense is separated from regular ErrorBoundary, So you can’t catch a Promise in componentDidCatch. When a Promise exception is thrown in a component, React will look up the most recent girl to process it, and if it is not found, React will throw an error.


The code above is pretty easy to understand, right? Regardless of the actual implementation of React, it’s clearly much more complex inside, and not something that all developers need to care about. With the simple code above, at least we know what Suspense behavior looks like. Now to test:


function ComponentThatThrowError() {
  throw new Error('error from component')
  return <div>throw error</div>
}

function App() {
  return (
    <div className="App">
      <ErrorBoundary>
        <Suspense fallback={null}>{/* Suspense does not catch exceptions other than promises, so it will be caught by ErrorBoundary */}<ComponentThatThrowError />
        </Suspense>
      </ErrorBoundary>
      <ErrorBoundary>{/* If an asynchronous operation fails, this ErrorBoundary catches the exception of an asynchronous operation */}<Suspense fallback={<div>loading...</div>} > {/ * here can capture ComponentThatThrowPromise thrown by the Promise, and display the loading... * /}<ComponentThatThrowPromise />
        </Suspense>
      </ErrorBoundary>
    </div>)}Copy the code


The above code shows the basic use of Suspense as well as exception handling. You can actually run this instance with this CodeSandbox.


Now look at ComponentThatThrowResolvedPromise:

let throwed = false

function ComponentThatThrowResolvedPromise() {
  if(! throwed) {throw new Promise((res, rej) = > {
      setTimeout((a)= > {
        throwed = true
        res()
      }, 3000)})}return <div>throw promise.</div>
}
Copy the code

The main points of the above code are throwed and thrown New promises. In this component, we interrupt component rendering by throwing new Promise, which will wait for the Promise to be ready and then re-render it.

In order to avoid re-rendering, Promise is thrown again, resulting in an ‘infinite loop’. You need to use a ‘cache’ to indicate that the asynchronous operation is ready and to avoid throwing the exception again.

The above uses throwed global variables to cache the state of asynchronous operations. However, for components, the global state is anti-pattern, and the side effect is that the components cannot be reused. Also, if caching gets out of the component’s life cycle and becomes unmanageable, how do we know if caching is valid? How is the lifecycle of this cache controlled? .

Of course you can use Redux or another state manager to maintain these caches, but sometimes you don’t want to use a state manager.

Is it possible to cache these states within the component? The answer is no, at least not for now, as explained by the implementation of custom Suspense above: when Suspense switches to Pending, the original component tree is uninstalled and all component state is lost.

As frustrating as it sounds, moving asynchronous operations to Suspense takes a little more work.




Cache the asynchronous operation state of Suspense

As stated above, we cannot cache the state of an asynchronous operation inside a component, so now we can only cache it externally. Consider these options:

  • Global cache. For example, global variables, global state managers (e.g. Redux, Mobx)
  • Use the Context API
  • State is cached by the parent component

The latter two are described below


Use the Context API

Let’s use the Context API as an example to briefly show you how to cache the state of asynchronous operations in Suspense.

First define the states of asynchronous operations:

It’s a Promise state


export enum PromiseState {
  Initial,  // The initialization state is created for the first time
  Pending,  // Promise is in the pending state
  Resolved, // End normally
  Rejected, / / exception
}

// We will save the state in the Context
export interface PromiseValue {
  state: PromiseState
  value: any
}
Copy the code


Now create a React.Context specifically to cache asynchronous state. For the sake of simplicity, our Context is simple, just a key-value store:

interface ContextValues {
  getResult(key: string): PromiseValue
  resetResult(key: string): void
}

const Context = React.createContext<ContextValues>({} as any)

export const SimplePromiseCache: FC = props= > {
  const cache = useRef<Map<string, PromiseValue> | undefined> ()// Get cache based on key
  const getResult = useCallback((key: string) = > {
    cache.current = cache.current || new Map(a)if (cache.current.has(key)) {
      return cache.current.get(key)!
    }

    const result = { state: PromiseState.Initial, value: undefined }

    cache.current.set(key, result)
    return result
  }, [])

  // Reset the cache based on key C
  const resetResult = useCallback((key: string) = > {
    if(cache.current ! =null)  cache.current.delete(key)
  }, [])

  const value = useMemo((a)= > ({ getResult, resetResult, }), [])

  return <Context.Provider value={value}>{props.children}</Context.Provider>
}
Copy the code


After that, we create a usePromise Hooks to encapsulate asynchronous operations and simplify the cumbersome steps:

/** * @params PROM receives a Promise and performs an asynchronous operation * @params key Cache key * @return Returns an object containing the result of the request and a reset method that resets the cache and rerequests */
export function usePromise<R> (prom: Promise<R>, key: string) :{ data: R; reset: (a)= > void } {
  // To force rerendering of components
  const [, setCount] = useState(0)
  // Get the context value
  const cache = useContext(Context)

  // ⚛️ Listen for key changes and resend the request
  useEffect(
    (a)= > {
      setCount(c= > c + 1)
    },
    [key],
  )

  // ️⚛️ Asynchronous processing
  // Retrieve the cache from the Context
  const result = cache.getResult(key)
  switch (result.state) {
    case PromiseState.Initial:
      // ⚛️ Initial state
      result.state = PromiseState.Pending
      result.value = prom
      prom.then(
        value= > {
          if (result.state === PromiseState.Pending) {
            result.state = PromiseState.Resolved
            result.value = value
          }
        },
        err => {
          if (result.state === PromiseState.Pending) {
            result.state = PromiseState.Rejected
            result.value = err
          }
        },
      )
      // Throw the promise and break the rendering
      throw prom
    case PromiseState.Pending:
      // ⚛️ is still in the request state. A task may be triggered by more than one component, and subsequent rendered components may be in the Pending state
      throw result.value
    case PromiseState.Resolved:
      // ⚛️ The operation is complete
      return {
        data: result.value,
        reset: (a)= > {
          cache.resetResult(key)
          setCount(c= > c + 1)}},case PromiseState.Rejected:
      // ⚛️ The exception ends and an error is thrown
      throw result.value
  }
}
Copy the code


There is nothing particularly difficult about the above code, which is to decide whether to throw a Promise or return the result of an asynchronous request, depending on the state of the current exception request.

To get started, first package the parent component of Suspense with SimplePromiseCache so that the child component can get the cache:

function App() {
  return (<SimplePromiseCache>
    <Suspense fallback="loading...">
      <DelayShow timeout={3000} />
    </Suspense>
  </SimplePromiseCache>)}Copy the code

Test:

function DelayShow({timeout}: {timeout: number}) {
  const { data } = usePromise(
    new Promise<number>(res= > {
      setTimeout((a)= > res(timeout), timeout)
    }),
    'delayShow'./ / the cache key
  )

  return <div>DelayShow: {data}</div>
}
Copy the code

The above code looks like this:


This section shows how to cache asynchronous operations using the Context API. This is a bit more complicated than you might think. Managing these caches manually can be a tricky problem (what to use as the cache key, how to determine if the cache is valid, and how to reclaim the cache). Not even React officials have a perfect answer, so let’s leave it to the community to explore.

Unless you’re the author of the library, it’s not too early for the average React developer to pay attention to these details, but there will soon be a lot of third-party libraries related to React data requests that will follow.

React has an experimental library called React-Cache, which currently uses an LRU global cache




Extract the cache state to the parent level

Since asynchronous states can’t be cached in children of Suspense, use parent components to avoid global states, have more flexibility in managing these states without having to worry about cache lifecycle management, and simplify the logic of the child components. Personally, I think this is a more universal approach than the Context API.


So, how do you do that? We create another createResource function based on usePromise, which is no longer a Hooks, but instead creates a resource object with the following signature:

function createResource<R> (prom: () = >Promise<R>) :Resource<R>
Copy the code


CreateResource Returns a Resource object:

interface Resource<R> {
  // Read 'resources' that are called in the child components of the Suspense package, just like usePromise above
  read(): R
  // ⚛️ added benefits of preloading
  preload(): void
}
Copy the code


The ⚛️Resource object is created in the parent component and passed to the child component via Props, which calls the read() method to read the data. A Resource is no different from a normal object to a subordinate component; it doesn’t know that this is an asynchronous request. That’s the beauty of Suspense!

Also, since the Resource object is created at the parent component, there is an added benefit: we can preload() to perform an asynchronous operation before displaying the child component.


CreateResource implementation:

export default function createResource<R> (prom: () = >Promise<R>) :Resource<R> {
  / / cache
  const result: PromiseValue = {
    state: PromiseState.Initial,
    value: prom,
  }

  function initial() {
    if(result.state ! == PromiseState.Initial) {return
    }
    result.state = PromiseState.Pending
    const p = (result.value = result.value())
    p.then(
      (value: any) = > {
        if (result.state === PromiseState.Pending) {
          result.state = PromiseState.Resolved
          result.value = value
        }
      },
      (err: any) => {
        if (result.state === PromiseState.Pending) {
          result.state = PromiseState.Rejected
          result.value = err
        }
      },
    )
    return p
  }

  return {
    read() {
      switch (result.state) {
        case PromiseState.Initial:
          // ⚛️ Initial state
          // Throw the promise and break the rendering
          throw initial()
        case PromiseState.Pending:
          // ⚛️ is still in the request state. A task may be triggered by more than one component, and subsequent rendered components may be in the Pending state
          throw result.value
        case PromiseState.Resolved:
          // ⚛️ The operation is complete
          return result.value
        case PromiseState.Rejected:
          // ⚛️ The exception ends and an error is thrown
          throw result.value
      }
    },
    / / preload
    preload: initial,
  }
}
Copy the code


The use of createResource is also simple: create a Resource at the parent component and pass it to the child component via Props. Since only one Tab can be displayed at a time, we can choose to preload the remaining Tabs to make them open faster:

const App = (a)= > {
  const [active, setActive] = useState('tab1')
  / / create the Resource
  const [resources] = useState((a)= > ({
    tab1: createResource((a)= > fetchPosts()),
    tab2: createResource((a)= > fetchOrders()),
    tab3: createResource((a)= > fetchUsers()),
  }))

  useEffect((a)= > {
    // Preload undisplayed Tab data
    Object.keys(resources).forEach(name= > {
      if(name ! == active) { resources[name].preload() } }) }, [])return (<div className="app">
    <Suspense fallback="loading...">
      <Tabs active={active} onChange={setActive}>
        <Tab key="tab1"><Posts resource={resources.tab1}></Posts></Tab>
        <Tab key="tab2"><Orders resource={resources.tab2}></Orders></Tab>
        <Tab key="tab3"><Users resource={resources.tab3}></Users></Tab>
      </Tabs>
    </Suspense>
  </div>)}Copy the code


Let’s pick a random sub-component and look at its implementation:

const Posts: FC<{resource: Resource<Post[]>}> = ({resource}) = > {
  const posts = resource.read()

  return (<div className="posts">
    {posts.map(i => <PostSummary key={i.id} value={i} />)}
  </div>)}Copy the code


Ok, that’s a lot better than the Context API, and I personally prefer that. In this mode, because resources are passed in from outside, component behavior is determined and easy to test and reuse.


However, both have application scenarios:

  • The Context API pattern is suitable for third-party data request libraries such as Apollo and Relay. In this mode, the API is more concise and elegant. Refer to the Relay API
  • The createResource pattern is more suitable for ordinary developers to encapsulate their asynchronous operations.




Concurrent initiation of requests


As shown in the figure above, this is often the case in real projects. A complex interface may have data from multiple interfaces. For example:

/** * User information page */
function ProfilePage() {
  const [user, setUser] = useState(null);

  // Get the user information first
  useEffect((a)= > {
    fetchUser().then(u= >setUser(u)); } []);if (user === null) {
    return <p>Loading profile...</p>;
  }

  return (
    <>
      <h1>{user.name}</h1>
      <ProfileTimeline />
    </>); } / function ProfileTimeline() {const [posts, setPosts] = useState(null); useEffect(() => { fetchPosts().then(p => setPosts(p)); } []); if (posts === null) { return<h2>Loading posts...</h2>;
  }

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.text}</li>
      ))}
    </ul>
  );
}
Copy the code


The code examples above are from official documentation. The above code fetchUser and fetchPosts are loaded sequentially. We want the page to be loaded as soon as possible. There are two solutions to solve this problem:

  • 1️ mentions fetchPosts to the upper level and uses themPromise.allConcurrent load
  • 2️ Extract the two into independent components and become brothers rather than fathers and sons. This can be rendered concurrently and thus requests concurrently initiated


First take a look at 1️ :

function fetchProfileData() {
  // Use promise All for concurrent loading
  return Promise.all([
    fetchUser(),
    fetchPosts()
  ]).then(([user, posts]) = > {
    return{user, posts}; })}const promise = fetchProfileData();
function ProfilePage() {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState(null);

  useEffect((a)= > {
    promise.then(data= >{ setUser(data.user); setPosts(data.posts); }); } []);if (user === null) {
    return <p>Loading profile...</p>;
  }
  return(<> <h1>{user.name}</h1> {/* ProfileTimeline posts={posts} /> </>); }Copy the code


That looks good, but there’s a hard part to this:

  • ① Asynchronous requests should be raised and then usedPromise.allParcel, I feel very troublesome, complex page how to do?
  • Now the load time depends on the longest operation performed in promise. all. FetchPosts might take a long time to load, but fetchUser should be done pretty quickly, and if fetchUser is done first, you should at least let the user see the user first.


1️ scheme is not particularly good, take a look at 2️ scheme:

function ProfilePage() {
  return (<div className="profile-page">
    <ProfileDetails />
    <ProfileTimeline />
  </div>)}Copy the code


2️ scheme is the best way before Suspense. ProfileDetails is responsible for loading user information and ProfileTimeline is responsible for loading time line. The two are executed concurrently without interference.

However, there are drawbacks: page loading will have two load indicators. Can they be merged? It is possible that the ProfileTimeline is completed first, while the ProfileDetails are still spinning.


Now for plan 3️ : Suspense 🎉

const resource = fetchProfileData();

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

function ProfileDetails() {
  const user = resource.user.read();
  return <h1>{user.name}</h1>;
}

function ProfileTimeline() {
  const posts = resource.posts.read();
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.text}</li>
      ))}
    </ul>
  );

Copy the code


When React renders the ProfilePage, it returns the ProfileDetails and ProfileTimeline.

Render the ProfileDetails first. If the resource is not fully loaded, throw a Promise exception to break the ProfileDetails rendering.

React then tries to render the ProfileTimeline, again throwing a Promise exception.

Finally React finds ProfileDetails recent Suspense, showing Loading Profile…

Like scenario 2️, Suspense supports concurrent request invocation and it addresses some of the drawbacks of scenario 2️ : there is only one load indicator and it won’t show up if ProfileTimeline completes first.

More than that, you’ll find a more flexible display strategy for Suspense loading state.




To deal with race

Even if the Javascript is single-threaded, you may need to handle race states, mainly because the timing of asynchronous operations cannot be guaranteed.

Don’t keep me in suspense. Give me an example. There is a component that relies on an id passed in from the outside to get data asynchronously:

function UserInfo({id}: {id: string}) {
  const [user, setUser] = useState<User|undefined> ()/** * ⚛️ Listens for id changes and initiates requests */
  useEffect((a)= > {
    fetchUserInfo().then(user= > setUser(user))
  }, [id])

  return user == null ? <Loading /> : renderUser(user)
}
Copy the code


What’s wrong with the above code? If the ID changes multiple times, multiple requests will be made, but the order in which these requests are completed is not guaranteed, which will lead to a race, where the first request may be completed last, which will result in incorrect data being presented on the page.


How to solve it? It is also easy to solve, using a mechanism like optimistic locking. We can save the ID of this request. If the ids are inconsistent at the end of the request, it means that a new request has been initiated:

function UserInfo({id}: {id: string}) {
  const [user, setUser] = setState<User|undefined> ()const currentId = useRef<string>()

  /** * ⚛️ Listens for id changes and initiates requests */
  useEffect((a)= > {
    currentId.current = id
    fetchUserInfo().then(user= > {
      // If the id is inconsistent, it indicates that a new request has been initiated
      if(id ! == currentId.current) {return
      }

      setUser(user)
    })
  }, [id])

  return user == null ? <Loading /> : renderUser(user)
}
Copy the code


Suspense doesn’t have race problems in Suspense, so the code above is implemented with Suspense as follows:

function UserInfo({resource}: {resource: Resource<User>}) {
  const user = resource.read()
  return renderUser(user)
}
Copy the code


Damn, it’s so simple! What is passed to UserInfo is a simple object with no race.

What about its superior components?


function createUserResource(id: string) {
  return {
    info: createResource((a)= > fecthUserInfo(id)),
    timeline: createResource((a)= > fecthTimeline(id)),
  }
}

function UserPage({id}: {id: string}) {
  const [resource, setResource] = useState((a)= > createUserResource(id))

  // ⚛️ migrate the id listener here
  useEffect((a)= > {
    // Reset the resource
    setResource(createUserResource(id))
  }, [id])

  return(<div className="user-page"> <Suspense loading="Loading User..." > <UserInfo resource={resource.info} /> <Timeline resource={resource.timeline} /> </Suspense> </div>) }Copy the code


The asynchronous request is transformed into a ‘resource object’, which in this case is just an ordinary object, passing it by Props, which perfectly solves the race problem of asynchronous requests…


Another problem with Suspense is that after asynchronous actions have been performed our pages may have changed and React will throw exceptions by setting component state: Can’t perform a React state update on an unmounted Component




Error handling

What if the asynchronous request is abnormal? As mentioned in the principles of Suspense implementation section above, React will throw an exception if the asynchronous request fails and we can catch it through ErrorBoundary mechanism.

Let’s write a higher-order component to simplify the process of Suspense and exception handling:

export default function sup<P> (fallback: NonNullable
       
        , catcher: (err: any
       ) = >NonNullable<React.ReactNode>,){
  return (Comp: React.ComponentType<P>) = >{ interface State { error? : any }class Sup extends React.Component<P.State> {
      state: State = {}

      // Catch an exception
      static getDerivedStateFromError(error: any) {
        return { error }
      }

      render() {
        return (
          <Suspense fallback={fallback}>
            {this.state.error ? catcher(this.state.error) : <Comp {. this.props} / >}
          </Suspense>
        )
      }
    }

    return Sup
  }
}
Copy the code


To use:

// UserInfo.js

const UserInfo: FC<UserInfoProps> = (props) = > {/ *... * /}

export defaultSup (<Loading text=" Loading text ") />, (err) => <ErrorMessage error={err} /> )(UserInfo)Copy the code


A little less boilerplate code, still a little more concise, right? .




Suspense choreography

If pages have a lot of Suspense, then many circles are spinning around and the user experience isn’t great.

However, it’s not easy to combine them directly because each block has different load priorities and life cycles, and it’s not good to force them into a girl. Such as:

function UserPage() {
  return(<Suspense fallback="loading..." > <UserInfo resource={infoResource} /> <UserPost resource={postResource} /> </Suspense>) }Copy the code

If UserPost needs to be paginated, each click on the next page causes the entire UserPage loading… This must be unacceptable…


So the Concurrent schema introduces a new API, SuspenseList, to orchestrate multiple load states for Suspense. We can choose the display policy of loading state according to the actual scenario. For example,

function Page({ resource }) {
  return( <SuspenseList revealOrder="forwards"> <Suspense fallback={<h2>Loading Foo... </h2>}> <Foo resource={resource} /> </Suspense> <Suspense fallback={<h2>Loading Bar... </h2>}> <Bar resource={resource} /> </Suspense> </SuspenseList> ); }Copy the code


Suppose Foo is loaded in 5s and Bar is completed in 2s. The effects of various choreographed combinations of SuspenseList are as follows:

You can go through thisCodeSandbox sampleexperience


RevealOrder indicates the order of display. It currently has three options: forwards, Backwards, and Together

  • forwards– Display from front to back. In other words, the previous one is not loaded, and the later one will not be displayed. Even when asynchronous operations are completed in Suspense later, you have to wait for the previous execution to complete
  • backwards– Opposite to forwards, shown back to front.
  • together– Show together after all Suspense loads have completed


In addition, SuspenseList has another attribute, tail, which controls whether to fold these girls and has three default values, collapsed and hidden

  • Default value – Display all
  • collapsed– Fold to show only the first girl being loaded
  • hidden– No loading status is displayed


In addition, SuspenseList can be combined, and the subordinate of SuspenseList can contain other suspenselists.




conclusion

The main character of this article is Suspense. If React Hooks are the logic reuse primitive provided by React and ErrorBoundary is the exception capture primitive, then Suspense will be the asynchronous operation primitive for React. Suspense + ErrorBoundary simplifies manual handling of load states and exception states.

Suspense can be understood to mean to interrupt rendering, or to pause rendering. We briefly discussed the implementation principle of Suspense, which simply uses the exception throwing mechanism of ErrorBoundary to interrupt rendering and restore the rendering of components after asynchronous operations are completed.

However, when the component rerenders (reentrant), all state is lost and the asynchronous processing state cannot be stored locally in the component, so you have to look outside and cache the asynchronous processing state in a global or parent component.

Some people will say React is not pure and functional enough. I’m not a big fan of functional programming. I think it doesn’t matter which programming paradigm is better at solving the problem. Since React Hooks came out, there have been no such thing as pure functional components. For Suspense, the createResource pattern can also make a component’s behavior predictable and testable. Other pain points, or to further practice and verification.

Suspense is exciting because it not only solves some of the old asynchronous processing problems, but it also brings new ways of developing. Eager students can try it out in their own lab projects.




The resources

  • Suspense for Data Fetching (Experimental)
  • Concurrent Rendering in React – Andrew Clark and Brian Vaughn – React Conf 2018
  • React: Realization and discussion of Suspense
  • react-cache