Enhancing React Native Apps with Optimistic UI: Mastering useOptimistic and useTransition


With the release of React 19, two powerful hooks—useOptimistic
and useTransition
—have significantly enhanced user experience by making UI updates smoother and more responsive. These hooks efficiently manage state updates, ensuring users don’t encounter UI freezes or unnecessary delays during asynchronous operations. In this blog, I’ll guide you through how these hooks work and demonstrate their implementation in a React Native app with a practical example.
Introduction to useOptimistic
and useTransition
useOptimistic
This hook allows you to temporarily update the UI assuming that an operation will succeed, even before receiving a confirmation from the backend. If the operation fails, you can handle the error and revert the state accordingly.
useTransition
This hook helps manage state updates without blocking the UI, keeping it responsive while executing async tasks. It defers rendering updates, ensuring that other UI interactions remain smooth even when network calls are in progress.
Implementing an Optimistic UI Update in React Native
Let’s walk through an example where we optimistically update a user’s name in a React Native app.
Full Code Example
import React, { useOptimistic, useTransition, useState } from 'react';
import { View, Text, Button } from 'react-native';
export default function App() {
// useTransition to manage async state updates without blocking UI
const [isPending, startTransition] = useTransition();
// useState to hold the actual confirmed name
const [name, setName] = useState('John');
// useOptimistic to provide an immediate UI update
const [optimisticName, setOptimisticName] = useOptimistic(name);
function createControlledPromise(shouldResolve, value) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldResolve) {
resolve({ value, error: null });
} else {
reject({ value: null, error: 'Promise was rejected' });
}
}, 2500);
});
}
const submitAction = () => {
startTransition(async () => {
setOptimisticName('John Doe'); // Temporary UI update
try {
const result = await createControlledPromise(false, 'John Doe'); // Change to `true` to test success
console.log('Final Value is:', result.value);
// Persist the confirmed value in useState
setName(result.value);
} catch (error) {
console.error(error.error);
setOptimisticName(name); // Revert to the original state on failure
}
});
};
return (
<View>
<Text>Your name is: {optimisticName}</Text>
<Button disabled={isPending} onPress={submitAction} title='Change Name' />
</View>
);
}
Breakdown of Important Lines of Code
1. Managing UI Updates Efficiently
const [isPending, startTransition] = useTransition();
- This ensures that UI updates related to async operations do not block the main UI thread, preventing potential lag.
2. Handling State and Optimistic UI Updates
const [name, setName] = useState('John');
const [optimisticName, setOptimisticName] = useOptimistic(name);
name
holds the final confirmed value.optimisticName
is used to immediately reflect UI changes before confirmation.
3. Creating a Simulated API Call
function createControlledPromise(shouldResolve, value) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldResolve) {
resolve({ value, error: null });
} else {
reject({ value: null, error: 'Promise was rejected' });
}
}, 2500);
});
}
- This function mimics an API call with a delay and can either resolve or reject based on
shouldResolve
.
4. Triggering the Optimistic UI Update
startTransition(async () => {
setOptimisticName('John Doe'); // Temporary UI update
setOptimisticName
immediately updates the UI before the API response.
5. Handling API Response and Errors
try {
const result = await createControlledPromise(false, 'John Doe');
setName(result.value);
} catch (error) {
setOptimisticName(name); // Revert on failure
}
- If the API call fails, we revert
optimisticName
to the original value.
How It Works
Initial State: The
name
state is set to'John'
.Optimistic Update: When the button is pressed,
useOptimistic
temporarily updates the UI to show'John Doe'
.API Simulation: The
createControlledPromise
function simulates a network request, which can either resolve (success) or reject (failure).State Update Handling:
If the promise resolves,
name
is updated inuseState
.If it fails, an error message is logged, but the UI already reflected the change optimistically.
Key Benefits of This Approach
Smooth User Experience: Users see immediate UI updates without waiting for API calls.
Non-blocking UI:
useTransition
ensures the app remains responsive during async operations.Error Handling: If the operation fails, the UI is reverted to the previous state seamlessly.
Enhancing the Implementation
To further improve this, we can:
Show a loading indicator while
isPending
is true.Display an error message instead of just logging it.
Handle retries in case of failures.
Example: Showing a Loading Indicator
{isPending && <ActivityIndicator size='small' color='#0000ff' />}
useOptimistic
vs. useState
: The Debate
You might be wondering: why use useOptimistic
when we can simply use useState
and manually update the value before the API call?
1. The Traditional useState
Approach (Manually Reverting State)
Instead of useOptimistic
, you could do:
const [name, setName] = useState('John');
const submitAction = async () => {
const previousName = name;
setName('John Doe'); // Update UI immediately
try {
const result = await createControlledPromise(false, 'John Doe'); // Simulating API call
setName(result.value); // Set final confirmed value
} catch (error) {
setName(previousName); // Revert UI if API fails
console.error(error.error);
}
};
Downsides of useState
Approach:
Manually tracking state (
previousName
) can be error-prone.Race conditions might occur if multiple updates happen quickly.
If multiple async actions update the same state, rolling back can be messy.
2. Why useOptimistic
is Better
React 19 introduced useOptimistic
to handle exactly this case in a cleaner way:
const [name, setName] = useState('John');
const [optimisticName, setOptimisticName] = useOptimistic(name);
const submitAction = () => {
startTransition(async () => {
setOptimisticName('John Doe'); // Temporary optimistic update
try {
const result = await createControlledPromise(false, 'John Doe');
setName(result.value); // Persist final value
} catch (error) {
console.error(error.error);
}
});
};
Benefits of useOptimistic
:
✅ No need to manually store and revert state—React automatically keeps track of the "real" state. ✅ No race conditions—React ensures that updates are applied in order, even if multiple updates happen quickly. ✅ Cleaner code—Your UI update logic is separate from the async function, making it easier to read.
3. When to Use useOptimistic
vs. useState
Approach | Pros | Cons |
Manually updating useState | Simple, doesn't require React 19 | Can lead to race conditions, requires manual rollback |
Using useOptimistic | Cleaner, prevents race conditions, React handles state tracking | Only available in React 19+ |
Yes, you can use useState
alone and manually revert the value, but useOptimistic
makes optimistic updates more structured, safer, and less error-prone. If you're using React 19, it's better to use useOptimistic
for handling UI updates before async actions complete.
Using useOptimistic
allows for fluid, non-blocking UI updates, making it ideal for applications where responsiveness and perceived speed are crucial. Try integrating this pattern in your projects and enhance your users' experience!
Conclusion
In conclusion, the introduction of useOptimistic
and useTransition
hooks in React 19 marks a significant advancement in managing state updates and asynchronous operations. These hooks enhance the responsiveness and user-friendliness of applications by allowing for smooth, non-blocking UI updates. By adopting these hooks, developers can provide a more seamless and engaging user experience, making applications feel faster and more intuitive. Implementing these patterns in your projects can greatly improve the perceived performance and overall satisfaction of your users.
Subscribe to my newsletter
Read articles from Fiyaz Hussain directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Fiyaz Hussain
Fiyaz Hussain
Empowering Developers and Delighting Clients in Mobile Application Development, server-side Development and skillfully leveraging Google Cloud services for 4 Years and Counting!