React Performance: Using Modern Hooks for Smooth UI Interactions
Add to your RSS feed6 January 20254 min readTable of Contents
Modern web applications demand increasing interactivity, responsiveness, and speed. To meet these demands, the React team has developed tools that allow developers to finely control rendering and user experience. If you’re familiar with traditional optimization techniques such as useMemo
, useCallback
, memoization with React.memo
, and other common strategies, the following hooks may intrigue you:
useTransition
: Prioritizes rendering by splitting updates into critical and background tasks.useDeferredValue
: Defers updating heavy computations to prevent UI freezing during data input.useOptimistic
: Simplifies implementing optimistic updates out of the box.
This article explores the key concepts of these hooks with practical examples to demonstrate how and when to use them.
useTransition
: Rendering Prioritization for Smooth UI
Concept
User actions like typing, switching tabs, or clicking buttons can trigger heavy state updates such as filtering large collections or recalculating complex data. If these updates are processed immediately with high priority, the UI may freeze momentarily, causing delays in user interaction.
Introduced in React 18, useTransition
allows marking updates as “non-critical” or “transitionary.” This keeps the key UI interactions responsive while heavy updates are processed later in the background.
Basic Example
In the following example, text input updates the field instantly, ensuring responsiveness, while filtering a large list (filteredItems
) is wrapped in startTransition
, enabling React to process it in the background.
1 import React, { useState, useTransition } from 'react';23 function BigList({ items }) {4 return (5 <ul>6 {items.map((item) => (7 <li key={item.id}>{item.text}</li>8 ))}9 </ul>10 );11 }1213 export default function App() {14 const [text, setText] = useState('');15 const [filteredItems, setFilteredItems] = useState([]);16 const [isPending, startTransition] = useTransition();1718 const allItems = Array.from({ length: 10000 }, (_, i) => ({19 id: i,20 text: `Item ${i}`,21 }));2223 const handleInputChange = (e) => {24 const value = e.target.value;25 setText(value);2627 startTransition(() => {28 const filtered = allItems.filter((item) =>29 item.text.toLowerCase().includes(value.toLowerCase())30 );31 setFilteredItems(filtered);32 });33 };3435 return (36 <div>37 <h1>List: {filteredItems.length} items</h1>38 <input39 value={text}40 onChange={handleInputChange}41 placeholder="Search the list..."42 />43 {isPending && <p>Loading...</p>}44 <BigList items={filteredItems} />45 </div>46 );47 }
How It Works
- Responsive Input: The text state updates immediately, ensuring the input field remains responsive.
- Background Updates: The filteredItems update is wrapped in startTransition, allowing React to process it later if the user continues typing.
- Loading Indicator: The isPending state shows an indicator while the transition is in progress.
Use Cases for useTransition
- Filtering/sorting large lists.
- Redrawing complex components (e.g., maps with numerous objects).
- Smooth animations during screen or tab transitions.
Caveats
useTransition
doesn’t cancel computations; it adjusts priority.- For extremely heavy logic, consider additional optimizations like memoization or moving computations to a Web Worker.
- Overusing
useTransition
may lead to noticeable delays in non-critical updates.
useDeferredValue
: Lazy Updates for Heavy Data
Concept
useDeferredValue
, introduced in React 18, is useful when you want dual states:
- Immediate State: Updates immediately, ensuring a responsive UI.
- Deferred State: Updates with lower priority, avoiding unnecessary re-renders during intensive tasks.
Example
Here, a text input updates instantly, while a deferred value is passed to a heavy component, minimizing UI lag.
1 import React, { useState, useDeferredValue, memo } from 'react';23 const SearchResults = memo(function SearchResults({ searchTerm }) {4 const allItems = Array.from({ length: 5000 }, (_, i) => `Item ${i}`);56 const filteredItems = allItems.filter((item) =>7 item.toLowerCase().includes(searchTerm.toLowerCase())8 );910 return (11 <ul>12 {filteredItems.map((item, idx) => (13 <li key={idx}>{item}</li>14 ))}15 </ul>16 );17 });1819 export default function App() {20 const [inputValue, setInputValue] = useState('');21 const deferredValue = useDeferredValue(inputValue);2223 return (24 <div>25 <input26 value={inputValue}27 onChange={(e) => setInputValue(e.target.value)}28 placeholder="Search..."29 />30 <SearchResults searchTerm={deferredValue} />31 </div>32 );33 }
How It Works
inputValue
updates instantly, keeping the input field responsive.deferredValue
lags behind, allowing React to optimize rendering.
Differences from useDebounce
useDebounce
: Adds a time-based delay before state updates.useDeferredValue
: Dynamically adjusts rendering priority without explicit time delays.
useOptimistic
: Simplifying Optimistic Updates
Concept
Optimistic updates immediately reflect user actions in the UI while the server processes the changes. If the server fails, the UI reverts to its original state. React 19’s useOptimistic simplifies this process.
Example
In this example, new orders are optimistically added to the list, giving users instant feedback.
1 import { useOptimistic, useState, useRef } from 'react';23 async function makeOrder(orderName) {4 await new Promise((res) => setTimeout(res, 1500));5 return orderName;6 }78 function Kitchen({ orders, onMakeOrder }) {9 const formRef = useRef();1011 const [optimisticOrders, addOptimisticOrder] = useOptimistic(12 orders,13 (state, newOrder) => [...state, { orderName: newOrder, preparing: true }]14 );1516 async function formAction(formData) {17 const orderName = formData.get("orderName");18 addOptimisticOrder(orderName);19 formRef.current.reset();20 await onMakeOrder(orderName);21 }2223 return (24 <div>25 <form action={formAction} ref={formRef}>26 <input type="text" name="orderName" placeholder="Enter order" />27 <button type="submit">Order</button>28 </form>29 {optimisticOrders.map((order, index) => (30 <div key={index}>31 {order.orderName} {order.preparing ? '(Preparing...)' : '(Ready!)'}32 </div>33 ))}34 </div>35 );36 }3738 export default function App() {39 const [orders, setOrders] = useState([]);4041 async function onMakeOrder(orderName) {42 const sentOrder = await makeOrder(orderName);43 setOrders((orders) => [...orders, { orderName: sentOrder }]);44 }4546 return <Kitchen orders={orders} onMakeOrder={onMakeOrder} />;47 }
Advantages
- Simplifies optimistic state management.
- Built-in rollback mechanism for server errors.
Use Cases
- Instant feedback for comments, likes, or messages.
- Adding items to a shopping cart.
Conclusion
useTransition
: Marks non-critical updates for background processing.useDeferredValue
: Defers rendering heavy computations for smoother UI.useOptimistic
: Simplifies optimistic UI updates with rollback capability.
Understanding and using these hooks effectively can significantly enhance the responsiveness of your React applications.