Thank you for supporting ayqY personal subscription number, every week free to push a (only Unique One) original quality blog, topics including but not limited to front-end, Node, Android, math (WebGL), Chinese (afterreading extra-curate books), English (document translation). You can go to http://blog.ayqy.net and have a good time

Writing in the front

After Suspense, useTransition comes

Don’t you have Suspense enough?

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

In Suspense subtrees, as long as there are Lazy components that have not come back, the content specified by fallback will be taken, which is equivalent to loading that can be promoted to any ancestor level.

Suspense components can be placed anywhere above Lazy components (in the component tree) and can have multiple Lazy components below.

In terms of loading scenarios alone, Suspense offers two abilities:

  • Support loading lifting

  • Support loading aggregation

There are two benefits to the user experience:

  • Avoid layout jitter (when data comes back, a piece of content pops up)

  • Treat different network environments differently (no loading at all if data is returned quickly)

The former is the benefit of loading (or skeleton), while the latter benefits from intermittent scheduling under Concurrent Mode

P.S. for details on Suspense, see React Suspense — starting with splitting code with loading…

Suspense’s elegant, flexible and user-friendly loading seems to have reached its ultimate development experience and user experience. However, further exploration reveals that loading still has several problems:

  • Will the experience be better with loading?

  • What’s wrong with loading immediately?

  • How to resolve the conflict between interactive real-time response and Loading?

  • Is there a perceptual way to get faster with long loading that cannot be cut down?

  • Is layout jitter really gone? What if there are more than one loading in the list?

Let’s take a look at each of these questions

2. Visually weakening loading

Will the experience be better with loading?

Taking a typical paged list as an example, a common interaction might look like this:

1. The content of the first page appears 2. Click the next page 3. Loading disappears after a period of time. 6. The second page is displayedCopy the code

The biggest problem is that the first page is not available (invisible, or hidden) during loading. In other words, loading affects the integrity of the page content and the responsiveness of the application.

In this case, simply remove loading:

1. The first page appears 2. Click on the next page 3. There was no interactive feedback, and after a few seconds 4. The second page appearedCopy the code

The user experience is made worse by the lack of immediate interactive feedback. So, is there a way to have the best of both worlds to ensure responsiveness during loading and interactive experience similar to loading?

There is. Weakening the visual effect of loading:

  • Reduce global loading (or content block loading) to local loading to avoid damaging content integrity

  • Hint at old content by graying it: Avoid confusing users with old content

For example, for a button click scenario, you can simply add loading feedback to the button:

/ /... render() { const { isLoading } = this.state; return ( <Page> <Content style={{ color: isLoading ? "black" : "gray" }} /> <Button>{isLoading ? "Next" : "Loading..." }</Button> </Page> ); }Copy the code

It not only ensures that the user can still see the complete content during loading (although some content is old, it has been hinted by gray setting), but also provides immediate interactive feedback

Most of the time, it’s ok to show loading right away, as in the example above. However, in other cases, it’s not as good as you would expect

3. Logically lazy loading

What’s wrong with loading immediately?

If loading is very fast (100ms), the user may only feel something flash by… Another bad user experience

Of course, we usually do not add loading to such scenes, because loading usually brings users a kind of “slow” psychological expectation, and loading an operation that is already very fast will undoubtedly lower the user’s perception of speed experience, so we choose not to add loading

However, if there is an operation that can be extremely fast or slow, does loading add or not add?

At this time, loading should be carried out as required. For example, loading time should be delayed, and loading will be displayed only after 200ms when the new content is not ready

React took this scenario into account and created useTransition

useTransition

The Transition feature is provided in the form of the Hooks API:

const [startTransition, isPending] = React.useTransition({  timeoutMs: 3000});Copy the code

P.S. Note that the Transition feature relies on a Concurrent Mode and is currently (2019/11/23) not officially available (an experimental feature) and the API may change, for reference only, see Transitions for demos

The Transition Hook tells React that it’s okay to delay updating State:

Wrap state update into startTransition to tell React it’s okay to delay it.

Such as:

function App() { const [resource, setResource] = useState(initialResource); const [startTransition, isPending] = React.useTransition({ timeoutMs: 3000 }); return (<> <button disabled={isPending} onClick={() => { startTransition(() => { const nextUserId = getNextId(resource.userId); setResource(fetchProfileData(nextUserId)); }); }} > Next </button> {isPending ? " Loading..." : null} <ProfilePage resource={resource} /> </>); }function ProfilePage({ resource }) { return (<Suspense fallback={<h1>Loading profile... </h1>} > <ProfileDetails resource={resource} /> <Suspense fallback={<h1>Loading posts... </h1>} > <ProfileTimeline resource={resource} /> </Suspense> </Suspense>); }Copy the code
  1. Click the Next button to retrieve ProfileData immediately, and isPending becomes True to display Loading…

  2. If ProfileData comes back within 3 seconds, the new ProfilePage content is displayed (switching from the old ProfilePage that is being displayed)

  3. In Suspense fallback from ProfilePage, Loading profile is displayed…

In other words, startTransition has delayed (yet to be retrieved) resource status values that should have been passed to the ProfilePage right away by up to three seconds, which is exactly what we want loading on demand: TimeoutMs Loading is displayed only when loading is timed out

In Suspense, Transition can delay loading

On-demand loading

In terms of page content state, Transition introduces a Pending state where old content is still available:

The meanings of each state are as follows:

  • Receded: current page content has disappeared, demoted to Suspense fallback

  • Skeleton: New pages have appeared and some of the new content may still be loading

  • Pending: New content is Pending, the current page is complete and still interactive

The reason for Pending is to avoid going backwards (hiding something that already exists) :

However, The recrecede state is not very pleasant because it “hides” existing information. This is why React lets us opt into a Different sequence (Pending → Skeleton → Complete) with useTransition.

If the loading state is set to recede → Skeleton → Complete, then the loading state will be set to mymyra. > > Receded > Skeleton > Complete (if loading is not required)

So shorten Pending times to get into Skeleton as quickly as possible for optimal experience. A tip is to wrap slow and unimportant components in Suspense:

Instead of making the transition shorter, we can “disconnect” the slow component from the transition by wrapping it into

Best practices

At the same time, thanks to Hooks fine-grained logic reuse, it is easy to encapsulate Transition’s on-demand loading effects into basic components, such as Button:

function Button({ children, onClick }) { const [startTransition, isPending] = useTransition({ timeoutMs: 10000 }); function handleClick() { startTransition(() => { onClick(); }); } const spinner = ( // ... ) ; return (<> <button onClick={handleClick} disabled={isPending}> {children} </button> {isPending ? spinner : null} </>); }Copy the code

It is also officially recommended that the UI component library consider useTransition scenarios to reduce redundant code:

Pretty much any button click or interaction that can lead to a component suspending needs to be wrapped in useTransition to avoid accidentally hiding something the user is interacting with.

This can lead to a lot of repetitive code across components. This is why we generally recommend to bake useTransition into the design system components of your app.

Iv. Resolve the conflict between interactive real-time response and Loading

How to resolve the conflict between interactive real-time response and Loading?

The reason why Transition can delay loading display is that State update is delayed. What about State updates that cannot be deferred, such as input values:

function App() { const [query, setQuery] = useState(initialQuery); function handleChange(e) { const value = e.target.value; setQuery(value); } return (<> <input value={query} onChange={handleChange} /> <Suspense fallback={<p>Loading... </p>}> <Translation input={query} /> </Suspense> </>); }Copy the code

The input is used as a controlled component (user input is handled through onChange), so the new value must be updated to State immediately, otherwise input delays and even confusion will occur

The conflict arises, then, when the need for real-time response input and Transition delayed State updates seem impossible to coexist

The official solution is to create a redundant copy of the status value.

function App() { const [query, setQuery] = useState(initialQuery); const [resource, setResource] = useState(initialResource); const [startTransition, isPending] = useTransition({ timeoutMs: 5000 }); function handleChange(e) { const value = e.target.value; // Outside the transition (urgent) setQuery(value); startTransition(() => { // Inside the transition (may be delayed) setResource(fetchTranslation(value)); }); } return (<> <input value={query} onChange={handleChange} /> <Suspense fallback={<p>Loading... </p>}> <Translation resource={resource} /> </Suspense> </>); }Copy the code

React’s experience tells us to calculate when we can, share when we can, and avoid redundant state values. The benefit is to avoid possible omissions during state updates:

This lets us avoid mistakes where we update one state but forget the other state.

We just redundant a State value (query and resource), not to override the principle of practice, but to prioritize states:

  • High State: State that does not want its updates to be delayed, such as input values

  • Low optimal State: State requiring delay, such as Transition

So with Transition, State has priority

Consider sacrificing UI consistency

Is there a perceptual way to get faster with long loading that cannot be cut down?

There is. If you’re willing to sacrifice UI consistency

Yes, UI consistency is not untouchable, and if necessary, consider sacrificing UI consistency for a better perceived experience. It might be “irrelevant”, but it’s probably friendlier than loading for 10 seconds or more. Also, we can make the user aware of UI inconsistencies by means of grey hints

React provides a Custom Value Hook for this purpose

useDeferredValue

const deferredResource = React.useDeferredValue(resource, { timeoutMs: 1000}); <ProfileTimeline resource={deferredResource} isStale={deferredResource! == resource} />Copy the code

Note that useDeferredValue has not been officially released yet. The API is subject to change. See UseDeferring a Value for a sample

This is similar to the Transition mechanism, which is equivalent to delaying state updating. The old data can be used until the new data is ready. If a new data is available within 1 second, the new data will be displayed, otherwise the state will be updated immediately and the loading will be loaded

The difference is that useDeferredValue is for state values, and Transition is for state updates, which is an API and semantic difference, and the mechanics are very similar

Eliminate layout jitter completely

Is layout jitter really gone? What if there are more than one loading in the list?

In a scenario with multiple loading sequences, layout jitter may inevitably occur. Visually, we usually don’t want an existing piece to be pushed aside (visually appends, not inserts). To eliminate layout jitter completely, there are two approaches:

  • Display all list items at the same time: wait until all items are ready to display, but the wait time goes up

  • Control list items appear in their relative order: Inserts can be eliminated, and wait times are not always the worst

How do you control the order in which asynchronous content appears (or disappears)?

React takes this into account and provides SuspenseList to control the rendering order of Suspense content and ensure elements in your lists are displayed in a relative order to avoid crowding out content:

<SuspenseList> coordinates the “reveal order” of the Young <Suspense> Nodes below it

SuspenseList

import { SuspenseList } from 'react'; function ProfilePage({ resource }) { return ( <SuspenseList revealOrder="forwards"> <ProfileDetails resource={resource} /> <Suspense fallback={<h2>Loading posts... </h2>}> <ProfileTimeline resource={resource} /> </Suspense> <Suspense fallback={<h2>Loading fun facts... </h2>}> <ProfileTrivia resource={resource} /> </Suspense> </SuspenseList> ); }Copy the code

RevealOrder =”forward “indicates that the children under SuspenseList must be picked up top-down, no matter who’s data is ready first. Similarly, values can be backwards and together.

In addition, the tail option is available to avoid the experience of multiple loading at the same time. See SuspenseList for details

P.S. Note that SuspenseList is not officially available at this time (2019/11/23), and the specific API may change

7. To summarize

As we can see, React is going further in its pursuit of the ultimate experience:

  • Suspense: Support elegant, flexible, and user-friendly content degradation

  • UseTransition: Supports downgrading on demand, only when it is really slow

  • UseDeferredValue: Supports sacrificing UI consistency for a better perceived experience

  • SuspenseList: Supports controlling the order and number of degradation effects in a set

The easiest strategy for P.S. to downgrade is loading. Anything else, such as caching values, even an AD or a mini-game, is a downgrade

The resources

  • Concurrent UI Patterns (Experimental)

If you find any problems in this article, please check the original article and leave a comment. Ayqy will reply if you see it.