1. Actions: Making async operations cleaner
Transition notes
When you wrap an async operation in a transition (using useTransition), React will:
Immediately set isPending to true
Keep the current UI interactive while the async operation is happening
Apply the state updates only after the async operation completes
Handle pending states and errors automatically
Code sample with useActionState
// Old way
function ProfileForm() {
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState(null);
async function handleSubmit() {
setIsPending(true);
try {
await updateProfile();
setIsPending(false);
} catch (e) {
setError(e);
setIsPending(false);
}
}
}
// React 19 way with useActionState
function ProfileForm() {
const [error, submitAction, isPending] = useActionState(
async (prev, formData) => {
try {
await updateProfile(formData);
return null; // no error
} catch (e) {
return e; // return error
}
},
// initial state
null
);
return (
<form action={submitAction}>
{/* Form fields */}
<button disabled={isPending}>Submit</button>
{error && <p>{error}</p>}
</form>
);
}
Async function into its own function. Keep it clean. Just pass the function and then the initial state.
2. Optimistic Updates - Show changes instantly
So nice when error it automatically reverts to original state.
function TodoList() {
// Current state: ["Buy milk"]
const [optimisticTodos, setOptimisticTodos] = useOptimistic(todos);
async function addTodo(formData) {
const newTodo = formData.get("todo");
// Immediately show: ["Buy milk", "Buy eggs"]
setOptimisticTodos([...todos, newTodo]);
// If API fails, React automatically reverts to original state
await addTodoToServer(newTodo);
}
return (
<form action={addTodo}>
<input name="todo" />
<ul>
{optimisticTodos.map((todo) => (
<li>{todo}</li>
))}
</ul>
</form>
);
}
3. The use
Hook - Handle promises in render
function Comments({ commentsPromise }) {
// This will suspend rendering until comments load
const comments = use(commentsPromise);
return (
<ul>
{comments.map((comment) => (
<li key={comment.id}>{comment.text}</li>
))}
</ul>
);
}
// Use it with Suspense
<Suspense fallback={<Spinner />}>
<Comments commentsPromise={fetchComments()} />
</Suspense>;
"suspending" means temporarily pausing the rendering of a component while it waits for some data or resource to load. When a component suspends, React will show the nearest <Suspense>
boundary's fallback content until the data is ready.
4. Better ref handling
// Old way
const Input = forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});
// React 19 way - ref is just a prop!
function Input({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
// New: Cleanup function for refs
<div
ref={(node) => {
if (node) {
// Setup
const handler = () => console.log("clicked");
node.addEventListener("click", handler);
// Return cleanup
return () => {
node.removeEventListener("click", handler);
};
}
}}
/>;
Nice that refs have explicit cleanup functions. They run when component unmounts.
5. Simpler Context usage
// Old way
const ThemeContext = createContext('light');
<ThemeContext.Provider value="dark">
<App />
</ThemeContext.Provider>
// React 19 way
<ThemeContext value="dark">
<App />
</ThemeContext>
This is nice to see. Not that I had a big issue with it. But I always appreciate cleaner code.
6. Document Metadata in Components
This is nice especially if you wanna do meta tags dynamically based on the data.
function BlogPost({ post }) {
return (
<article>
<title>{post.title}</title>
<meta name="description" content={post.summary} />
<link rel="canonical" href={post.url} />
<h1>{post.title}</h1>
{post.content}
</article>
);
}
Since I use React Router 7 as a framework, this is handled for me there when using it lol.