Unlocking the Power of React 19: 10 Best Practices for Modern Development

Unlocking the Power of React 19: 10 Best Practices for Modern Development
React 19 is here, and it's more than just an incremental update—it's a paradigm shift. With the introduction of the React Compiler, Actions, and a suite of new hooks, the way we write performant and user-friendly applications has fundamentally evolved.
For years, we've manually optimized our apps with useMemo, useCallback, and complex state management for asynchronous operations. React 19 automates much of this, allowing us to focus on what truly matters: building great features with cleaner, more declarative code.
Here are 10 best practices to help you and your team leverage the full potential of React 19 in your next project.
1. Trust the Compiler: Write Simpler Code
The single biggest change in React 19 is the React Compiler. It's an optimizing compiler that automatically memoizes components and hooks. This means you no longer need to manually wrap functions in useCallback or values in useMemo to prevent unnecessary re-renders.
Old Way (Pre-Compiler):
import { useCallback, useState } from "react";
function OldComponent({ data }) {
const [text, setText] = useState("");
// Manually memoized to prevent re-creating the function on each render
const handleClick = useCallback(() => {
console.log("Clicked with data:", data);
}, [data]);
return (
<div>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<MyButton onClick={handleClick} />
</div>
);
}
React 19 Way (With Compiler):
import { useState } from "react";
// The compiler handles memoization automatically!
function NewComponent({ data }) {
const [text, setText] = useState("");
// Just a regular function. The compiler optimizes it.
const handleClick = () => {
console.log("Clicked with data:", data);
};
return (
<div>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<MyButton onClick={handleClick} />
</div>
);
}
Best Practice: Write straightforward JavaScript and let the compiler handle performance optimization. Only reach for manual memoization in the rare edge cases where the compiler might need a hint.
2. Embrace Actions for Data Mutations
Handling form submissions and data mutations has always involved boilerplate for managing pending, error, and success states. Actions streamline this entire process. You can pass a function (often an async serverAction) directly to a <form>'s action prop.
// actions.js
"use server"; // For server actions with frameworks like Next.js
export async function updateUser(userId, formData) {
const name = formData.get("name");
// ... logic to update user in the database
// This can throw an error or return data
}
// ProfilePage.jsx
import { updateUser } from "./actions";
function ProfilePage({ userId }) {
// Bind the userId to the server action
const updateUserWithId = updateUser.bind(null, userId);
return (
<form action={updateUserWithId}>
<label htmlFor="name">Name:</label>
<input type="text" id="name" name="name" />
<button type="submit">Update</button>
</form>
);
}
Best Practice: Use Actions for any form submission or data mutation. This simplifies state management and automatically handles pending states.
3. Enhance UX with useOptimistic
Optimistic updates dramatically improve perceived performance. The useOptimistic hook lets you immediately show the result of an action before the server confirms it. If the action fails, the UI automatically reverts.
import { useOptimistic } from "react";
import { sendComment } from "./actions";
function Comments({ comments }) {
const [optimisticComments, addOptimisticComment] = useOptimistic(
comments,
(state, newComment) => [...state, { text: newComment, sending: true }],
);
const formAction = async (formData) => {
const newComment = formData.get("comment");
addOptimisticComment(newComment);
await sendComment(newComment);
};
return (
<div>
<ul>
{optimisticComments.map((c, i) => (
<li key={i}>
{c.text} {c.sending && <small>(Sending...)</small>}
</li>
))}
</ul>
<form action={formAction}>
<input type="text" name="comment" />
<button type="submit">Send</button>
</form>
</div>
);
}
Best Practice: Pair useOptimistic with Actions to give users instant feedback on their interactions.
4. Decouple Forms with useFormStatus
How does a submit button know it should be disabled or show "Submitting..."? Previously, this required prop drilling or context. The useFormStatus hook lets a component inside a <form> access the status of that form.
import { useFormStatus } from "react-dom";
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Submitting..." : "Submit"}
</button>
);
}
// Used inside any <form>
function MyForm() {
return (
<form action={someAction}>
<input name="field" />
<SubmitButton />
</form>
);
}
Best Practice: Use useFormStatus to create reusable, decoupled UI components (like buttons and spinners) that react to form state without receiving any props.
5. Simplify Async Logic with the use Hook
The use hook is a powerful new way to read the value of a promise or context within a component. When used with a promise inside a <Suspense> boundary, it makes asynchronous data fetching look synchronous.
import { Suspense, use } from "react";
// Assume fetchUserData returns a promise
const userDataPromise = fetchUserData();
function ProfileDetails() {
// The component will "suspend" here until the promise resolves
const user = use(userDataPromise);
return <h1>{user.name}</h1>;
}
function ProfilePage() {
return (
<Suspense fallback={<h2>Loading profile...</h2>}>
<ProfileDetails />
</Suspense>
);
}
Best Practice: In Suspense-enabled data fetching patterns, prefer use(promise) over useEffect and useState for cleaner, more readable async code.
6. Think in Server Components (with Frameworks)
While not part of the React library itself, React 19 is built with Server Components in mind. Frameworks like Next.js make them a core part of the architecture. Server Components run on the server, have zero impact on your client-side bundle size, and can access backend resources directly.
// app/page.js in Next.js - This is a Server Component by default
async function getProducts() {
const res = await db.query("SELECT * FROM products");
return res.rows;
}
export default async function HomePage() {
const products = await getProducts();
return (
<main>
<h1>Our Products</h1>
<ProductList products={products} />
</main>
);
}
Best Practice: For static content and initial data fetching, default to Server Components. Use Client Components ("use client") only when you need interactivity, state, or browser-only APIs.
7. Prioritize Component Composition
This is a timeless React principle, but it's more important than ever. With the compiler reducing the need for complex hooks, we can focus on building small, reusable components that are composed together using the children prop.
// A reusable Card component that knows nothing about its content
function Card({ children, title }) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-content">{children}</div>
</div>
);
}
// Composing it to build a specific feature
function UserProfile({ user }) {
return (
<Card title={user.name}>
<p>Email: {user.email}</p>
<p>Member since: {user.joinDate}</p>
</Card>
);
}
Best Practice: Build small, single-responsibility components. Use the children prop to create flexible and reusable layouts and containers.
8. Adopt a Scalable Project Structure
As projects grow, a well-organized file structure is crucial. Instead of grouping files by type (/components, /hooks), group them by feature or domain. This co-locates related logic, making it easier to navigate and maintain.
/src
/features
/authentication
- LoginButton.jsx
- auth.actions.js
- useAuthStatus.js
- index.js
/products
- ProductList.jsx
- ProductCard.jsx
- products.actions.js
- index.js
/components // (For truly shared, generic components)
- Button.jsx
- Card.jsx
Best Practice: Organize your code by feature. This makes your codebase more modular and easier to reason about as it scales.
9. Integrate TypeScript from Day One
React 19's new APIs, especially Actions, have excellent TypeScript support. Starting a project with TypeScript catches bugs before they happen, improves autocompletion, and makes refactoring safer. Given your backend experience with Node.js, you'll appreciate the type safety across your full stack.
Best Practice: Use TypeScript in all new React projects. It's the industry standard for building robust, maintainable applications.
10. Write Meaningful, User-Centric Tests
Focus your tests on how a user interacts with your application, not on the internal implementation details. With the compiler changing how your components are optimized, testing implementation is more brittle than ever.
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import MyForm from "./MyForm";
test("form submits and shows a success message", async () => {
render(<MyForm />);
// Simulate user typing into the input
await userEvent.type(screen.getByLabelText(/name/i), "Orfeas");
// Simulate user clicking the submit button
await userEvent.click(screen.getByRole("button", { name: /submit/i }));
// Assert that the success message appears
expect(await screen.findByText(/thank you/i)).toBeInTheDocument();
});
Best Practice: Use tools like React Testing Library and Vitest/Jest to write tests that simulate real user behavior. Test what the user sees and does, not the component's internal state.
Conclusion
React 19 invites us to write less code to achieve more. By embracing the compiler, using the new action-oriented hooks, and thinking in terms of server-first architecture, we can build faster, more resilient applications with a significantly improved developer experience.
Happy coding