React 19 introduces several powerful new hooks that revolutionize how we handle forms and manage optimistic updates in our applications. In this blog, we'll explore useFormStatus
, useActionState
, and useOptimistic
- three hooks that make our React applications more responsive and user-friendly.
useFormStatus: Enhanced Form Handling
The useFormStatus
hook provides real-time information about form submissions, making it easier to create responsive and accessible forms. Let's explore how this hook improves upon React 18's form handling capabilities.
Example 1: Basic Form Loading State
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
);
}
function SignupForm() {
return (
<form action={async (formData) => {
await submitSignupData(formData);
}}>
<input name="email" type="email" />
<input name="password" type="password" />
<SubmitButton />
</form>
);
}
In React 18, you'd need to manually manage loading states using useState. The new useFormStatus hook automatically handles this, reducing boilerplate code.
Example 2: Multiple Form States
function FormStatus() {
const { pending, data, method } = useFormStatus();
return (
<div role="status">
{pending && <span>Submitting via {method}...</span>}
{!pending && data && <span>Last submission: {new Date().toLocaleString()}</span>}
</div>
);
}
function ContactForm() {
return (
<form action={async (formData) => {
await submitContactForm(formData);
}}>
<textarea name="message" />
<FormStatus />
<SubmitButton />
</form>
);
}
Example 3: Form Validation Status
function ValidationStatus() {
const { pending, validationErrors } = useFormStatus();
return (
<div role="alert">
{validationErrors?.map((error, index) => (
<p key={index} className="error">{error}</p>
))}
</div>
);
}
function RegistrationForm() {
return (
<form action={async (formData) => {
const errors = validateRegistration(formData);
if (errors.length) throw errors;
await register(formData);
}}>
<input name="username" />
<input name="email" type="email" />
<ValidationStatus />
<SubmitButton />
</form>
);
}
Example 4: Multi-Step Form Progress
function FormProgress() {
const { pending, step, totalSteps } = useFormStatus();
return (
<div className="progress-bar">
<div
className="progress"
style={{width: `${(step / totalSteps) * 100}%`}}
/>
<span>Step {step} of {totalSteps}</span>
</div>
);
}
function WizardForm() {
return (
<form action={async (formData) => {
await submitWizardData(formData, { steps: 3 });
}}>
{/* Form steps */}
<FormProgress />
<SubmitButton />
</form>
);
}
Example 5: File Upload Progress
function UploadProgress() {
const { pending, progress } = useFormStatus();
return (
<div>
{pending && progress && (
<div className="upload-progress">
<div
className="progress-bar"
style={{width: `${progress}%`}}
/>
<span>{progress}% uploaded</span>
</div>
)}
</div>
);
}
function FileUploadForm() {
return (
<form action={async (formData) => {
await uploadFile(formData);
}}>
<input type="file" name="document" />
<UploadProgress />
<SubmitButton />
</form>
);
}
useActionState: Managing Action Results
The useActionState
hook provides a way to track the state of form actions and server mutations, making it easier to handle success and error states.
Example 1: Basic Action State
function SubmissionStatus() {
const state = useActionState();
return (
<div>
{state.status === 'success' && <p>Submission successful!</p>}
{state.status === 'error' && <p>Error: {state.error.message}</p>}
</div>
);
}
function CommentForm() {
return (
<form action={async (formData) => {
await submitComment(formData);
}}>
<textarea name="comment" />
<SubmissionStatus />
<SubmitButton />
</form>
);
}
Example 2: Action History
function ActionHistory() {
const state = useActionState();
return (
<div>
<h3>Recent Actions</h3>
<ul>
{state.history.map((action, index) => (
<li key={index}>
{action.type} - {action.timestamp}
{action.status === 'error' && ` (Failed: ${action.error.message})`}
</li>
))}
</ul>
</div>
);
}
Example 3: Retry Mechanism
function RetryableAction() {
const state = useActionState();
return (
<div>
{state.status === 'error' && (
<button
onClick={() => state.retry()}
disabled={state.retrying}
>
{state.retrying ? 'Retrying...' : 'Retry'}
</button>
)}
</div>
);
}
Example 4: Action Queue
function ActionQueue() {
const state = useActionState();
return (
<div>
<h3>Pending Actions</h3>
{state.queue.map((action, index) => (
<div key={index}>
{action.type} - Queued at {action.queuedAt}
<button onClick={() => action.cancel()}>Cancel</button>
</div>
))}
</div>
);
}
Example 5: Action Statistics
function ActionStats() {
const state = useActionState();
return (
<div>
<h3>Action Statistics</h3>
<p>Success Rate: {state.stats.successRate}%</p>
<p>Average Duration: {state.stats.avgDuration}ms</p>
<p>Total Actions: {state.stats.total}</p>
</div>
);
}
useOptimistic: Smooth UI Updates
The useOptimistic
hook enables immediate UI updates while waiting for server responses, creating a more responsive user experience.
Example 1: Optimistic Todo List
function TodoList() {
const [todos, setTodos] = useState([]);
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo) => [...state, newTodo]
);
async function addTodo(formData) {
const newTodo = {
id: Date.now(),
text: formData.get('todo'),
completed: false
};
addOptimisticTodo(newTodo);
await saveTodo(newTodo);
}
return (
<div>
<form action={addTodo}>
<input name="todo" />
<button type="submit">Add Todo</button>
</form>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
}
Example 2: Optimistic Like Button
function LikeButton({ postId, initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
const [optimisticLikes, addOptimisticLike] = useOptimistic(
likes,
(state) => state + 1
);
async function handleLike() {
addOptimisticLike();
await likePost(postId);
}
return (
<button onClick={handleLike}>
{optimisticLikes} Likes
</button>
);
}
Example 3: Optimistic Comment Thread
function CommentThread({ postId }) {
const [comments, setComments] = useState([]);
const [optimisticComments, addOptimisticComment] = useOptimistic(
comments,
(state, newComment) => [...state, newComment]
);
async function submitComment(formData) {
const comment = {
id: Date.now(),
text: formData.get('comment'),
pending: true
};
addOptimisticComment(comment);
await saveComment(postId, comment);
}
return (
<div>
<form action={submitComment}>
<textarea name="comment" />
<button type="submit">Comment</button>
</form>
{optimisticComments.map(comment => (
<div key={comment.id} style={{ opacity: comment.pending ? 0.5 : 1 }}>
{comment.text}
</div>
))}
</div>
);
}
Example 4: Optimistic Shopping Cart
function ShoppingCart() {
const [cart, setCart] = useState([]);
const [optimisticCart, updateOptimisticCart] = useOptimistic(
cart,
(state, update) => {
const { type, item } = update;
switch (type) {
case 'add':
return [...state, item];
case 'remove':
return state.filter(i => i.id !== item.id);
case 'update':
return state.map(i => i.id === item.id ? item : i);
default:
return state;
}
}
);
async function updateCart(type, item) {
updateOptimisticCart({ type, item });
await saveCart({ type, item });
}
return (
<div>
{optimisticCart.map(item => (
<div key={item.id}>
{item.name} - ${item.price}
<button onClick={() => updateCart('remove', item)}>Remove</button>
</div>
))}
</div>
);
}
Example 5: Optimistic User Settings
function UserSettings() {
const [settings, setSettings] = useState({});
const [optimisticSettings, updateOptimisticSetting] = useOptimistic(
settings,
(state, update) => ({
...state,
[update.key]: update.value
})
);
async function updateSetting(key, value) {
updateOptimisticSetting({ key, value });
await saveSettings({ [key]: value });
}
return (
<div>
<div>
<label>
Theme:
<select
value={optimisticSettings.theme}
onChange={e => updateSetting('theme', e.target.value)}
>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</label>
</div>
<div>
<label>
Notifications:
<input
type="checkbox"
checked={optimisticSettings.notifications}
onChange={e => updateSetting('notifications', e.target.checked)}
/>
</label>
</div>
</div>
);
}
Remember to check the official React documentation for the most up-to-date information and best practices when using these hooks in your applications.
Happy Coding!
Top comments (0)