Skip to content
2

New feature: startTransition #41

New feature: startTransition #41
Jun 8, 2021 · 10 comments · 31 replies

@rickhanlonii
rickhanlonii Jun 8, 2021
Maintainer

Overview

In React 18 we’re introducing a new API that helps keep your app responsive even during large screen updates. This new API lets you substantially improve user interactions by marking specific updates as “transitions”. React will let you provide visual feedback during a state transition and keep the browser responsive while a transition is happening.

What problem does this solve?

Building apps that feel fluid and responsive is not always easy. Sometimes, small actions like clicking a button or typing into an input can cause a lot to happen on screen. This can cause the page to freeze or hang while all of the work is being done.

For example, consider typing in an input field that filters a list of data. You need to store the value of the field in state so that you can filter the data and control the value of that input field. Your code may look something like this:

// Update the input value and search results
setSearchQuery(input);

Here, whenever the user types a character, we update the input value and use the new value to search the list and show the results. For large screen updates, this can cause lag on the page while everything renders, making typing or other interactions feel slow and unresponsive. Even if the list is not too long, the list items themselves may be complex and different on every keystroke, and there may be no clear way to optimize their rendering.

Conceptually, the issue is that there are two different updates that need to happen. The first update is an urgent update, to change the value of the input field and, potentially, some UI around it. The second, is a less urgent update to show the results of the search.

// Urgent: Show what was typed
setInputValue(input);

// Not urgent: Show the results
setSearchQuery(input);

Users expect the first update to be immediate because the native browser handling for these interactions is fast. But the second update may be a bit delayed. Users don't expect it to complete immediately, which is good because there may be a lot of work to do. (In fact, developers often artificially delay such updates with techniques like debouncing.)

Until React 18, all updates were rendered urgently. This means that the two state states above would still be rendered at the same time, and would still block the user from seeing feedback from their interaction until everything rendered. What we’re missing is a way to tell React which updates are urgent, and which are not.

How does startTransition help?

The new startTransition API solves this issue by giving you the ability to mark updates as “transitions”:

import { startTransition } from 'react';


// Urgent: Show what was typed
setInputValue(input);

// Mark any state updates inside as transitions
startTransition(() => {
  // Transition: Show the results
  setSearchQuery(input);
});

Updates wrapped in startTransition are handled as non-urgent and will be interrupted if more urgent updates like clicks or key presses come in. If a transition gets interrupted by the user (for example, by typing multiple characters in a row), React will throw out the stale rendering work that wasn’t finished and render only the latest update.

Transitions lets you keep most interactions snappy even if they lead to significant UI changes. They also let you avoid wasting time rendering content that's no longer relevant.

What is a transition?

We classify state updates in two categories:

  • Urgent updates reflect direct interaction, like typing, clicking, pressing, and so on.
  • Transition updates transition the UI from one view to another.

Urgent updates like typing, clicking, or pressing, need immediate response to match our intuitions about how physical objects behave. Otherwise they feel “wrong”. However, transitions are different because the user doesn’t expect to see every intermediate value on screen.

For example, when you select a filter in a dropdown, you expect the filter button itself to respond immediately when you click. However, the actual results may transition separately. A small delay would be imperceptible and often expected. And if you change the filter again before the results are done rendering, you only care to see the latest results.

In a typical React app, most updates are conceptually transition updates. But for backwards compatibility reasons, transitions are opt-in. By default, React 18 still handles updates as urgent, and you can mark an update as a transition by wrapping it into startTransition.

How is it different from setTimeout?

A common solution to the above problem is to wrap the second update in setTimeout:

// Show what you typed
setInputValue(input);

// Show the results
setTimeout(() => {
  setSearchQuery(input);
}, 0);

This will delay the second update until after the first update is rendered. Throttling and debouncing are common variations of this technique.

One important difference is that startTransition is not scheduled for later like setTimeout is. It executes immediately. The function passed to startTransition runs synchronously, but any updates inside of it are marked as “transitions”. React will use this information later when processing the updates to decide how to render the update. This means that we start rendering the update earlier than if it were wrapped in a timeout. On a fast device, there would be very little delay between the two updates. On a slow device, the delay would be larger but the UI would remain responsive.

Another important difference is that a large screen update inside a setTimeout will still lock up the page, just later after the timeout. If the user is still typing or interacting with the page when the timeout fires, they will still be blocked from interacting with the page. But state updates marked with startTransition are interruptible, so they won’t lock up the page. They lets the browser handle events in small gaps between rendering different components. If the user input changes, React won’t have to keep rendering something that the user is no longer interested in.

Finally, because setTimeout simply delays the update, showing a loading indicator requires writing asynchronous code, which is often brittle. With transitions, React can track the pending state for you, updating it based on the current state of the transition and giving you the ability to show the user loading feedback while they wait.

What do I do while the transition is pending?

As a best practice, you’ll want to inform the user that there is work happening in the background. For that, we provide a Hook with an isPending flag for transitions:

import { useTransition } from 'react';


const [isPending, startTransition] = useTransition();

The isPending value is true while the transition is pending, allowing you to show an inline spinner while the user waits:

{isPending && <Spinner />}

The state update that you wrap in a transition doesn't have to originate in the same component. For example, a spinner inside the search input can reflect the progress of re-rendering the search results.

Why not just write faster code?

Writing faster code and avoiding unnecessary re-renders are still good ways to optimize performance. Transitions are complementary to that. They let you keep the UI responsive even during significant visual changes — for example, when showing a new screen. That's hard to optimize with existing strategies. Even when there's no unnecessary re-rendering, transitions still provide a better user experience than treating every single update as urgent.

Where can I use it?

You can use startTransition to wrap any update that you want to move to the background. Typically, these type of updates fall into two categories:

We’ll be posting again soon to cover each of these use cases with specific examples. Let us know if you have any questions!

Replies

10 comments
·
31 replies
1
// Mark any state updates inside as transitions
React.startTransition(() => {
  // Transition: Show the results
  setSearchQuery(input);
});

I know this is just a simplified example and data fetching isn’t the point, but isn’t this a data fetching anti-pattern since it implies that the data won’t be fetched until the render?

I wonder if it would be possible to update the example to have best practices for data without derailing too much from the point, just to avoid anyone accidentally believing they should do this.

5 replies
@gaearon

Hmm. I don't think I fully understand the concern.

The example was primarily about local filtering, not remote. But you're right a remote example would parallel it. (We'll be posting a separate note about that.) For that use case, the pattern still makes sense to me — whether or not you fetch on render, you'd be doing that by doing a state update in a transition. Even if you avoid fetch on render by using Server Components, you'd still kick it off by changing the "route" by setting state in a transition. For example, in the Server Components demo, clicking on a note "navigates" to it using the same mechanism:

https://github.com/reactjs/server-components-demo/blob/629ebda61d085428efd4a5f67f096ca2dfe15a1c/src/SidebarNote.client.js#L53-L59

@devknoll

Even if you avoid fetch on render by using Server Components, you'd still kick it off by changing the "route" by setting state in a transition

Is that a recommended practice? Or should a "route" also be a "resource" that is fetch-then-rendered? Unclear to me when to use one over the other 😅

@gaearon

gaearon Jun 8, 2021
Maintainer

Let's discuss this when we post a parallel post for network updates? Since this one is primarily about large re-renders, I think mixing up topics might be a bit confusing to future readers. Alternatively, maybe you could post it as a top-level question? Regardless of what triggers the fetch, you'd set state in a transition, so that doesn't change.

1

I like this explanation approach overall. I think it's going to be clearer and more understandable than the whole "multiple priorities" thing that's been floating around for the last couple years.

2 replies
@markerikson

Two things that might be worth expanding on:

  • How does this relate to the now-always-on batching behavior?
  • It's not entirely clear how having the "large state update" inside the transition is actually interruptible. Is that statement based more on the assumption that there are many individual components that have to be re-rendered (and thus can be paused after N components are processed), as opposed to having 1 component or some callback logic that happens to require a very long time to process? Like, if I have a component that had a multi-second while loop to simulate really slow rendering for that component, I would expect it to still end up blocking the main thread without being interruptible.
@gaearon

How does this relate to the now-always-on batching behavior?

Since urgent updates flush soon, they don't get as much batching as transitions (which happen over a longer period of time). There might be some deeper implementation-level differences but these are implementation details and probably not worth getting into.

It's not entirely clear how having the "large state update" inside the transition is actually interruptible.

By "large" we mean "many individual components, each of which takes a small slice of the time". In practice we very rarely see a single component being a bottleneck — usually, the cost is in the entire subtree, where each individual component contributes just a little bit (<1 ms), but it adds up. I answer this in more detail in the first part of #38 (comment).

1

@cassidoo
cassidoo Jun 8, 2021
Collaborator

Is this function meant to be used in components just within a <Suspense /> component? Or can it be used anywhere?

2 replies
@sebmarkbage

It can be used anywhere. Note that it's not just for suspending on data. It's for managing rendering asynchronously and other high level async concepts. For example, anything that animates should ideally go into one of these.

If you do load data and suspend during this update, this can technically avoid having to use a Suspense boundary because it'll just wait until all the data is done. However, since it can be tricky to guarantee that data only suspends in these particular scenarios it's good to have a Suspense boundary anyway. We also consider it to be best-practice in most scenarios to have a Suspense boundary on new loading screens so that the UI navigates immediately to where you're going. So technically you wouldn't need to have one in this scenario but you probably should.

@gaearon

gaearon Jun 8, 2021
Maintainer

To further clarify, we will be posting two separate deep dives about:

  • Using startTransition to wait for slow renders
  • Using startTransition to wait for network

The API is the same, but it handles both cases (and even both combined). We'll include sandboxes to show it.

1

What's the reason to include both React.startTransition and useTransition? If showing a loading indictor is a best practice, why providing React.startTransition that does not provide such capability?

7 replies
@sebmarkbage

This is a great question! We didn't add it for a long time for that reason. However, there are a few scenarios where you're doing updates in library code where this can get tricky and there's no obvious way in the UI where this belongs.

If you do a refresh buttons or pull-to-refresh UI, then it makes sense to show a loading indicator. However, if your library is automatically refreshing data in the background or updating the UI for consistency everywhere after a refetch, then there's no particular source in the UI where that indicator should be and the update may not be originating in the UI. That's the canonical use case for React.startTransition.

@lintonye

In that case I guess it's still possible to use useTransition -- just discard the first return value. Or, does React.startTransition come with less overhead?

@rickhanlonii

If you do use the hook without using the isPending flag then there's a bit of unnecessary work because we still re-render to update the isPending value. Another main difference is that you can only use useTransition inside of a component.

1

How do various transitions get orchestrated? Is it the order in which they occur? Or is there some consideration given to the changes necessary?

2 replies
@acdlite

Right now they all get batched together but the plan is to change this before the final release.

The idea is that each separate transition should be allowed to finish independently from the others — unless we detect that two transitions are related, in which case we'll batch them together. The mechanism we use to detect this is if multiple transitions update the same state queue (i.e. useState, useReducer, or class component). We call this "entanglement" and we use it to prevent us from showing intermediate states.

Like if I switch between tabs really quickly, React shouldn't bother to show anything except the last tab that I clicked. We do this by entangling all the tab navigations into a single batch.

I shared a few more implementation details here, if you're curious: #27 (comment)

@laurieontech

Thanks! That's helpful.

1

Is there anything functionally different between the following:

startTransition(() => {
  setDependent1(value)
  setDependent2(value)
})

and

startTransition(() => {
  setDependent1(value)
})
startTransition(() => {
  setDependent2(value)
})

My intuition is that in the first instance, dependent1 and dependent2 are executed in tandem, so they'll always be updated at the same time; but in the second instance, they're executed independent from each other, so for example if dependent2 is extremely expensive, dependent1 can update faster (but still possibly slower than without startTransition)

4 replies
@tesseralis

Or to put it another way: how much control do I have over how transitions are batched? I feel like both use cases -- making sure certain state updates happen in lockstep and making sure others are independent -- are useful.

@acdlite

acdlite Jun 9, 2021
Maintainer

Excellent question! The behavior here is a bit up in the air, for exactly the reasons you describe, so I'll share a bit of what we've been thinking.

Currently all transitions are always batched together. Even across multiple interactions. Mostly this is for legacy reasons — our earliest implementation was limited in its ability to run updates out-of-order, and even though we've since rewritten the model to give us more control, not all of that work is complete.

Conceptually, the model is that by marking something as a transition, it should be allowed to finish in parallel from other transitions — unless we detect that two transitions overlap. We determine that by whether they update one or more of the same state queue (useState, useReducer, class components).

If two transitions update the same queue, we "entangle" them so that they must finish together. This prevents us from showing an intermediate state. Like in a filtered list UI, if we switch filters really quickly, we should only ever update the list to correspond to the most recently selected filter.

If two transitions don't overlap, then we allow them to finish independently.

Ultimately I think we need both abilities: running multiple transitions in parallel, or batching updates into a single transition. The question is which is the default behavior, and how do you opt into the other.

Originally we thought that every startTransition should run in parallel. So in this example, these are treated as independent transitions:

startTransition(() => {
  navigateToNewTab();
});
startTransition(() => {
  dismissModal();
});

But, there's a problem of abstraction/composition. Because startTransition wraps around the execution of a function, there could be multiple startTransitions on the stack.

How should this work?

// Outer transition, added by some wrapper  function
startTransition(() => {
  // Nested transitions, called by some inner function that is wrapped by
  // an abstraction
  startTransition(() => {
    navigateToNewTab();
  });
  startTransition(() => {
    dismissModal();
  });
});

The outer caller probably expects all its updates to be batched together.

But the inner caller expects the transitions to be treated independently.

I expect this will be especially salient in the future when startTransition is used to spawn enter/exit animations.

We're still researching this space, but my current thinking is that we should default to batching all transitions that occur within a single browser event (click, press, keyboard input, etc), and all transitions that occur across separate browser events are treated independently.

Then we'd have an explicit opt-in for independent transitions that occur within the same browser event. Strawperson proposal:

// Not a real API, just brainstorming. These transitions would always be
// independent, even if wrapped by a parent transition.
startTransitions(
  () => {
    navigateToNewTab();
  },
  () => {
    dismissModal();
  },
)

Or something like that.

Since you've already thought about this, if you have some concrete UX examples in mind, that would be super helpful!

@tesseralis

My thinking for a use case is something the TypeScript playground. On user input, it loads a type-ahead, and also tries to compile the content of the whole page. I'd imagine wanting to specify that both these processes can be executed at a lower priority and independently of one another -- you wouldn't want the compiler to stall on trying to compile a page with a syntax error while you're trying to open up the type-ahead to figure how to fix the error!

1

On naming: Dan mentioned in a twitter thread that startTransition was named such because it may be used in a (far off) animation API. Two questions relating to this:

  • Even if it will be related to transitions, it still feels strange to me (not to mention confusion with useTransition hooks from react-spring). I feel like it makes more sense for it to name it something like:
nonUrgent(() => {
  setValue(something)
})
  • That said, could the React team speak a little about their ideas on how the startTransition API would be used for animation in the future? That may help bridge the gap in understanding and make the name make sense to me more.
4 replies
@acdlite

I'll let @sebmarkbage share more details if he wishes (it's still very early in the research process), but the gist of the idea is that React would support automatic enter/leave animations, but only if you opt-in with startTransition. We wouldn't do automatically it for urgent updates, like responding to controlled text input, because that could make the UI feel sluggish. You'd have to explicitly mark which updates are meant to be animated.

@acdlite

The key thing here related to animations is that the startTransition API gives developers a way to separate the urgent part of an interaction (e.g. updating a search input) from the less urgent part (e.g. updating a typeahead list of items that's connected to the search input). The distinction between these types of updates is useful not only for performance and responsiveness, but maps well onto which things should be animated.

@acdlite

The reason we're comfortable naming the whole thing startTransition is we anticipate a future where all transitions are animated, not just some. Like that's how iOS works; for many interactions, the developer doesn't have to do anything extra to animate things on and off the screen. Which is why native apps tend to feel more pleasant than web ones. You get nice transitions out of the box.

2

@markerikson
markerikson Jun 9, 2021
Collaborator

While it's not a question, I want to repeat and highlight something Dan showed over in the React issues to help explain how startTransition works conceptually:

let isInTransition = false

function startTransition(fn) {
  isInTransition = true
  fn()
  isInTransition = false
}

function setState(value) {
  stateQueue.push({
    nextState: value,
    isTransition: isInTransition
  })
}

See facebook/react#21649

2 replies
@rickhanlonii

rickhanlonii Jun 9, 2021
Maintainer Author

It's concise now, but if you're curious, here's what the in-progress research looked like before we simplified the model, and here it was mid-refactor to the new model!

1

@ad1992
ad1992 Jun 10, 2021
Collaborator

If there are multiple setState/useState inside startTransition do they get batched ?

If there are multiple startTransition and since set states inside it are interruptible so it may happen that updates from the first startTransition are applied after updates from nth startTransition or the ordering will always be the same as execution?

2 replies
@gaearon

If there are multiple setState/useState inside startTransition do they get batched ?

Yes, multiple setState calls inside the same startTransition will always get batched together. This means React will work on these updates combined and won't consider them separately.

If there are multiple startTransition and since set states inside it are interruptible so it may happen that updates from the first startTransition are applied after updates from nth startTransition or the ordering will always be the same as execution?

If you have multiple startTransition calls affecting the same piece of state, the last one always "wins". In other words, if you do:

startTransition(() => {
  setSearchQuery("h");
})

// very soon...
startTransition(() => {
  setSearchQuery("he");
})

// very soon...
startTransition(() => {
  setSearchQuery("hello");
})

React will only render "hello".

@ad1992

Ok so setState inside startTransition gets batched and the same piece of state is updated in multiple startTransition also gets batched.
What will happen if the states updated are different in each setTransition, for example 👇🏻

startTransition(() => {
  setSearchName("xyz");
})

startTransition(() => {
  setSearchSchool("Scholl name");
})

startTransition(() => {
  setSearchLang("javascript");
})

Will this be updated in DOM in the same order as they are executed (searchName -> searchSchool -> searchLang) or it can be any order?

1

One thing I found interesting in the old docs was the section about baking transitions into design systems / component libraries. I'm wondering if this is still the recommendation of the team after more experience integrating concurrent UI into Facebook.com, or whether leaving this decision to the application code has proven useful in some cases.

Some questions to start with:

  • What patterns have you found useful? Should most event handlers be wrapped in a transition or only certain ones? Which types of components should usually have builtin transitions?
  • Do you usually include transitions in low level components like a generic Button or make more specific components for narrower use cases? For generic components, I can see it being difficult to know whether the action that occurs when a user interacts with a component should be high or low priority.
  • How do you recommend transitions be integrated into controlled components. For example, when integrating transitions into a textfield, if you wrapped the onChange event in a transition and the component was controlled, then the update of the text in the textfield itself would also be lower priority as it depends on the parent component to re-render in response to the event. In the example above, you do this by storing two separate states for the input value and the current filter text, but I can see this being challenging for controlled components.
  • Many atomic components typically found in a design system don't usually have a place to include a loading spinner. Placing a spinner is often a concern of the application. In these cases, would you leave the transition up to the parent component to initiate so it also has access to the isPending state, or would you initiate it within the design system component and somehow expose this pending state elsewhere for the spinner to appear?
1 reply
@devongovett

Moved to a separate topic here: #63 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
Announcement
Labels
None yet
Beta