Overview
When building web applications we have to consider the performance aspect of it, there are different ways that we can optimize our React app's performance, like using useMemo,memo or other hooks and functions for memoization. But the useTransition hook is different from most other react hooks focused on performance, as it does not cache any data.
The useTransition allows us to mark some set state calls as lower priority than other regular set state calls,. These lower priority calls can be interrupted by react whenever some other thing needs to be updated first.
useTransition hook has the following signature :
const [isPending,startTransition] = useTransition();
The useTransition hook does not accept any arguments, and it returns an array with two values, similar to most other react hooks. Let's first focus on the second return value ( here referred to as startTransition ), the startTransition is a function which accepts one argument called action, any setState calls inside the action are marked as a transition, when a transition is executing the isPending value will be true.
Let's go deeper with some examples, you can find the source code at the end for all examples.
Example 1 : Without useTransition
We are creating a very simple app with three buttons, and three tabs. Clicking on a button will open it's tab
The code for the above image is :
App.tsx
import { useState } from 'react'
import './App.css'
import { Button } from './components/ui/button'
import About from './components/tabs/about'
import Messages from './components/tabs/messages'
import Friends from './components/tabs/friends'
type Tab = 'about' | 'messages' | 'friends'
function App() {
const [currentTab,setCurrentTab] = useState<Tab>('about')
const updateTab = (tab:Tab) => {
setCurrentTab(tab)
}
return (
<div className='min-w-screen min-h-screen bg-slate-500 py-20 px-10'>
<div className='w-full lg:w-3/4 mx-auto flex justify-center items-center gap-4'>
<Button className='cursor-pointer' onClick={()=>updateTab('about')} size={'lg'}>About</Button>
<Button className='cursor-pointer' onClick={()=>updateTab('messages')} size={'lg'}>Messages</Button>
<Button className='cursor-pointer' onClick={()=>updateTab('friends')} size={'lg'}>Friends</Button>
</div>
<div className='w-full max-w-lg mx-auto mt-10 bg-slate-400 min-h-[400px] p-4 rounded-lg'>
{currentTab == 'about' && <About/>}
{currentTab == 'messages' && <Messages/>}
{currentTab == 'friends' && <Friends/>}
</div>
</div>
)
}
export default App
And here is the code for the three tabs :
about.tsx
const About = () => {
return (
<div>This is the about tab!!</div>
)
}
export default About
friends.tsx
const Friends = () => {
return (
<div>This is the friends tab</div>
)
}
export default Friends
messages.tsx
const Messages = () => {
const messageList = []
for(let i=0; i<500; i++){
messageList.push(<Message m={`this is message ${i}`} key={i}/>)
}
return (
<div>{messageList}</div>
)
}
export default Messages
const Message = ({m}:{
m:string
}) => {
let startTime = performance.now();
while (performance.now() - startTime < 1) {
// Do nothing for 1 ms per item to emulate extremely slow code
}
return <p>{m}</p>
}
The about and friends tabs are just simple react components, but notice messages.tsx. Here we are artificially slowing down the component render to simulate an intensive and slow task.
If you click on the Messages button and then you immediately click on any other button, you will notice the app freezes for some time.
This is because we have slowed down messages.tsx component, and when we click on the messages button and then click on any other button the click to the other button is not handled by react until the messages component is rendered. This is not idea as we would like our app to be responsive and show immediate feedback to the user. We can fix this behaviour and improve our app's perceived performance by using the useTransition hook.
Example 2 : With useTransition
To utilize the benifits of transitions, we simple have to wrap our setState calls in an action and pass it to startTransition. Here is the updated App.tsx file :
App.tsx
import { useState, useTransition } from 'react'
import './App.css'
import { Button } from './components/ui/button'
import About from './components/tabs/about'
import Messages from './components/tabs/messages'
import Friends from './components/tabs/friends'
type Tab = 'about' | 'messages' | 'friends'
function App() {
const [isPending,startTransition] = useTransition()
const [currentTab,setCurrentTab] = useState<Tab>('about')
const updateTab = (tab:Tab) => {
// wrap setCurrentTab call in an action and pass it to startTransition
startTransition(()=>{
setCurrentTab(tab)
})
}
return (
<div className='min-w-screen min-h-screen bg-slate-500 py-20 px-10'>
<div className='w-full lg:w-3/4 mx-auto flex justify-center items-center gap-4'>
<Button className='cursor-pointer' onClick={()=>updateTab('about')} size={'lg'}>About</Button>
<Button className='cursor-pointer' onClick={()=>updateTab('messages')} size={'lg'}>Messages (Slow)</Button>
<Button className='cursor-pointer' onClick={()=>updateTab('friends')} size={'lg'}>Friends</Button>
</div>
<div className='w-full max-w-lg mx-auto mt-10 bg-slate-400 min-h-[400px] p-4 rounded-lg'>
{currentTab == 'about' && <About/>}
{currentTab == 'messages' && <Messages/>}
{currentTab == 'friends' && <Friends/>}
</div>
</div>
)
}
export default App
Just by doing this small change our app now feels much more responsive.
Notice if we now click on the messages button then immediately click on any other button, our app does not wait for the messages tab to render first and immediately we can show the other tab to the user. This is because useTransition has now marked our setCurrentTab call as a transition and it can now be interrupted.
The app is now responsive, but it would be much better if we can show user some feedback when the messages component is loading.
Example 3 : useTransition With Loading Indicators
the hook also returns us boolean variable indicating when a transition is taking place, we can use it display some feedback for when the messages tab is loading.
We create a new TabButton component and handle the transition logic in there, so we can display individual loading states for different button.
Here is the updated code :
tab-button.tsx
import React, { useTransition } from 'react'
import { Button } from './ui/button'
interface TabButtonProps {
action:()=>void,
children:React.ReactNode,
isActive:boolean
}
const TabButton = ({action,children,isActive}:TabButtonProps) => {
const [isPending,startTransition] = useTransition()
const performAction = () => {
startTransition(action)
}
return (
<Button className={`cursor-pointer ${isPending ? 'animate-pulse duration-300 ' : ''}`} onClick={performAction} size={'lg'} variant={isActive ? 'secondary' : 'default'}>{children}</Button>
)
}
export default TabButton
App.tsx
import { useState } from 'react'
import './App.css'
import About from './components/tabs/about'
import Messages from './components/tabs/messages'
import Friends from './components/tabs/friends'
import TabButton from './components/tab-button'
type Tab = 'about' | 'messages' | 'friends'
function App() {
const [currentTab,setCurrentTab] = useState<Tab>('about')
const updateTab = (tab:Tab) => {
setCurrentTab(tab)
}
return (
<div className='min-w-screen min-h-screen bg-slate-500 py-20 px-10'>
<div className='w-full lg:w-3/4 mx-auto flex justify-center items-center gap-4'>
<TabButton action={()=>updateTab('about')} isActive={currentTab == 'about'}>About</TabButton>
<TabButton action={()=>updateTab('messages')} isActive={currentTab == 'messages'}>Messages (Slow)</TabButton>
<TabButton action={()=>updateTab('friends')} isActive={currentTab == 'friends'}>Friends</TabButton>
</div>
<div className='w-full max-w-lg mx-auto mt-10 bg-slate-400 min-h-[400px] p-4 rounded-lg'>
{currentTab == 'about' && <About/>}
{currentTab == 'messages' && <Messages/>}
{currentTab == 'friends' && <Friends/>}
</div>
</div>
)
}
export default App
We have now moved the transition logic using isPending to the tab-button component, this enables us to show loading indicators when a transition is taking place :
We can now navigate across tabs without making the app freeze and show visual feedback when necessary.
Keep in mind that we have only used synchronous operations in startTransition, to use async operations you have to wrap the set state call in another startTransition. You can find more about it in the official react docs here.
You should now have a clear idea about how to work with useTransition hook š.