# React 19 Best Practices Guide (2024-2025)
**Report Date:** November 30, 2025
**Research Focus:** React 19 development best practices, new features, patterns, and security
---
## Table of Contents
1. [React 19 Overview](#react-19-overview)
2. [New Features and Migration Patterns](#new-features-and-migration-patterns)
3. [Component Patterns and Hooks Best Practices](#component-patterns-and-hooks-best-practices)
4. [Performance Optimization](#performance-optimization)
5. [TypeScript Integration Patterns](#typescript-integration-patterns)
6. [Testing React Components](#testing-react-components)
7. [Security Considerations](#security-considerations)
8. [Recommendations Summary](#recommendations-summary)
---
## React 19 Overview
React 19 was officially released on **December 5, 2024**, marking a significant milestone in React's evolution. This release focuses on enhancing performance, improving developer experience, and making asynchronous UI patterns production-ready.
### Key Highlights
- **Automatic Performance Optimization**: React Compiler (formerly React Forget) eliminates most manual memoization needs
- **Enhanced Async UI Handling**: New hooks specifically designed for form handling and optimistic updates
- **Server-First Architecture**: Production-ready Server Components and Server Actions
- **Concurrent Rendering by Default**: Prevents long renders from blocking the UI
- **Developer Experience**: Simplified APIs, better error reporting, and reduced boilerplate
---
## New Features and Migration Patterns
### 1. New Hooks
React 19 introduces four major hooks that revolutionize async UI patterns:
#### `use()` API
The `use()` API is a groundbreaking addition that reads resources during render. Unlike traditional hooks, it can be called conditionally.
**Key Features:**
- Works with Promises (suspends until resolution)
- Reads Context values
- Can be used inside `if` statements and loops (unlike traditional hooks)
- Must be used with Suspense for Promise handling
**Example with Promises:**
```javascript
import { use, Suspense } from 'react';
function DataComponent({ dataPromise }) {
const data = use(dataPromise); // Suspends until promise resolves
return
{data}
;
}
// Usage with Suspense
Loading...}>
```
**Example with Context (Conditional):**
```javascript
function MyComponent({ isSpecialMode }) {
if (isSpecialMode) {
const theme = use(ThemeContext); // Can use conditionally!
return
Special Mode
;
}
return
Normal Mode
;
}
```
**Important Note:** Promises created in render must be cached using a Suspense-compatible library.
#### `useActionState`
Simplifies async form handling by managing state transitions automatically.
**Syntax:**
```javascript
const [state, formAction] = useActionState(asyncFunction, initialState);
```
**Parameters:**
- `asyncFunction`: Receives current state and FormData
- `initialState`: Initial state value
**Example:**
```javascript
async function submitUser(prevState, formData) {
try {
const username = formData.get('username');
const newUser = await api.createUser(username);
return {
users: [...prevState.users, newUser],
error: null
};
} catch (error) {
return {
...prevState,
error: error.message
};
}
}
function UserForm() {
const [state, formAction] = useActionState(submitUser, {
users: [],
error: null
});
return (
);
}
```
#### `useOptimistic`
Enables optimistic UI updates that show expected results immediately while the server confirms.
**Syntax:**
```javascript
const [optimisticState, setOptimisticState] = useOptimistic(actualState, updateFn);
```
**Behavior:**
- Displays optimistic value immediately
- Automatically reverts if operation fails
- Updates to actual value on success
**Example:**
```javascript
function TodoList({ todos }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo) => [...state, { ...newTodo, sending: true }]
);
async function handleSubmit(formData) {
const title = formData.get('title');
addOptimisticTodo({ id: Date.now(), title });
try {
await api.createTodo(title);
// Success - optimistic update becomes permanent
} catch (error) {
// Failure - automatically reverts to original state
console.error('Failed to create todo:', error);
}
}
return (
<>
{optimisticTodos.map(todo => (
{todo.title}
))}
>
);
}
```
#### `useFormStatus`
Reads parent form status without prop drilling, similar to Context but specialized for forms.
**Important:** Must be imported from `react-dom`, not `react`:
```javascript
import { useFormStatus } from 'react-dom';
```
**Returns:** `{ pending, data }` where:
- `pending`: Boolean indicating if form is submitting
- `data`: FormData object with form values
**Example:**
```javascript
function SubmitButton() {
const { pending, data } = useFormStatus();
return (
{pending && data && (
Submitting {data.get('username')}...
)}
);
}
function Form() {
return (
);
}
```
### 2. Actions Framework
Actions are async functions that automatically manage:
- **Pending states**: Automatically tracked and reset
- **Error handling**: Integrated with Error Boundaries
- **Optimistic updates**: Via `useOptimistic`
- **Form resets**: Automatic reset after successful submission
**Form Integration:**
```javascript
async function createPost(formData) {
'use server'; // Server Action
const title = formData.get('title');
const post = await db.posts.create({ title });
revalidatePath('/posts');
return post;
}
function PostForm() {
return (
);
}
```
### 3. Server Components and Server Actions
**Server Components:**
- Execute on the server (build-time or per-request)
- Access databases directly
- Reduce JavaScript bundle size
- No interactivity (no state, effects, or event handlers)
**Server Actions:**
- Defined with `"use server"` directive
- Execute on the server
- Callable from Client Components
- Replace many REST/GraphQL API patterns
**Example:**
```javascript
// app/posts/actions.js
'use server';
export async function getPosts() {
const posts = await db.posts.findMany();
return posts;
}
export async function createPost(formData) {
const title = formData.get('title');
await db.posts.create({ title });
revalidatePath('/posts');
}
// app/posts/page.js (Server Component)
import { getPosts } from './actions';
export default async function PostsPage() {
const posts = await getPosts(); // Direct async/await
return (
Posts
{posts.map(post => (
{post.title}
))}
);
}
```
### 4. Developer Experience Improvements
#### `ref` as Prop (No More `forwardRef`)
Function components now accept `ref` directly:
```javascript
// React 19 - Simple and clean
function Input({ ref, ...props }) {
return ;
}
// React 18 - Verbose
const Input = forwardRef((props, ref) => {
return ;
});
```
#### Simplified Context Provider
```javascript
// React 19
// React 18
```
#### Ref Cleanup Functions
```javascript
function Component() {
return (
{
// Setup
element.addEventListener('scroll', handleScroll);
// Cleanup (new in React 19)
return () => {
element.removeEventListener('scroll', handleScroll);
};
}}>
Content
);
}
```
#### Enhanced `useDeferredValue`
```javascript
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query, {
initialValue: '' // Show empty initially, update in background
});
const results = search(deferredQuery);
return ;
}
```
### 5. Document Metadata Support
No more `react-helmet` needed:
```javascript
function BlogPost({ post }) {
return (
{post.title} - My Blog
{post.title}
{post.content}
);
}
```
### 6. Resource Preloading APIs
New APIs for performance optimization:
```javascript
import { preload, preinit, preconnect, prefetchDNS } from 'react-dom';
// Preload a resource
preload('/assets/font.woff2', { as: 'font', type: 'font/woff2' });
// Preinit (preload + execute)
preinit('/scripts/analytics.js', { as: 'script' });
// Preconnect to external domain
preconnect('https://cdn.example.com');
// DNS prefetch
prefetchDNS('https://api.example.com');
```
### Migration Patterns
#### Breaking Changes to Address
1. **PropTypes Removed**
- Migrate to TypeScript or remove PropTypes
- Function component `defaultProps` removed (use ES6 defaults)
2. **Legacy Context Removed**
- Migrate to modern `createContext` API
- Class components: use `contextType`
3. **String Refs Removed**
- Replace with callback refs: `ref={el => this.input = el}`
4. **ReactDOM Methods Removed**
- `ReactDOM.render()` → `createRoot().render()`
- `ReactDOM.hydrate()` → `hydrateRoot()`
- `unmountComponentAtNode()` → `root.unmount()`
- `findDOMNode()` → Use refs instead
5. **Test Utilities Removed**
- `react-test-renderer/shallow` → Use React Testing Library
- `react-dom/test-utils` → Use React Testing Library
#### Automated Migration
Use codemods for automatic migration:
```bash
# Comprehensive migration recipe
npx codemod@latest react/19/migration-recipe
# Or specific codemods
npx react-codemod replace-reactdom-render
npx react-codemod replace-string-ref
```
#### Incremental Adoption Strategy
1. **Update Dependencies:**
```bash
npm install --save-exact react@^19.0.0 react-dom@^19.0.0
npm install --save-exact @types/react@^19.0.0 @types/react-dom@^19.0.0
```
2. **Test in Staging:**
- Evaluate compatibility with your stack
- Run automated tests
- Test critical user flows
3. **Enable Features Gradually:**
- React Compiler can be enabled per-file/component
- Server Components require framework support (Next.js 15+)
- Most new hooks are opt-in
4. **Team Training:**
- Update internal documentation
- Conduct workshops on new patterns
- Share migration guide with team
---
## Component Patterns and Hooks Best Practices
### 1. Functional Components as Standard
**Recommendation:** Use functional components exclusively. Class components are legacy.
**Best Practice:**
```javascript
// Good - Modern functional component
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUser(userId).then(setUser).finally(() => setLoading(false));
}, [userId]);
if (loading) return ;
return
{user.name}
;
}
// Avoid - Class component (legacy)
class UserProfile extends React.Component {
// ...outdated pattern
}
```
### 2. Custom Hooks for Reusable Logic
Custom hooks are the preferred way to share stateful logic across components.
**Best Practice:**
```javascript
// Custom hook for data fetching
function useUser(userId) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetchUser(userId)
.then(data => {
if (!cancelled) {
setUser(data);
setError(null);
}
})
.catch(err => {
if (!cancelled) setError(err);
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => { cancelled = true; };
}, [userId]);
return { user, loading, error };
}
// Usage in multiple components
function UserProfile({ userId }) {
const { user, loading, error } = useUser(userId);
if (loading) return ;
if (error) return ;
return
);
}
```
### 5. Component Composition over Props Drilling
Use composition to avoid deep prop drilling:
```javascript
// Good - Composition
function Dashboard({ children }) {
const user = useUser();
return (
{children}
);
}
function App() {
return (
{/* Gets user from context, not props */}
);
}
// Avoid - Props drilling
function Dashboard({ user, children }) {
return (
{React.cloneElement(children, { user })}
);
}
```
### 6. Avoid Unnecessary useEffect
React 19 emphasizes reducing `useEffect` usage:
```javascript
// Bad - Unnecessary effect
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
setResults(filterResults(data, query));
}, [query, data]);
return ;
}
// Good - Direct calculation
function SearchResults({ query }) {
const results = useMemo(() => filterResults(data, query), [query, data]);
return ;
}
// Better - May not even need useMemo if fast
function SearchResults({ query }) {
const results = filterResults(data, query); // React Compiler handles this
return ;
}
```
### 7. Rules of Hooks (Still Apply)
1. **Only call hooks at the top level** - No conditionals, loops, or nested functions
2. **Only call hooks from React functions** - Components or custom hooks
3. **Custom hooks must start with "use"** - Naming convention for tooling
```javascript
// Bad - Conditional hook
function Component({ shouldFetch }) {
if (shouldFetch) {
const data = useFetch('/api/data'); // ERROR!
}
}
// Good - Conditional logic inside hook
function Component({ shouldFetch }) {
const data = useFetch(shouldFetch ? '/api/data' : null);
}
function useFetch(url) {
const [data, setData] = useState(null);
useEffect(() => {
if (!url) return;
fetch(url).then(r => r.json()).then(setData);
}, [url]);
return data;
}
```
---
## Performance Optimization
### 1. React Compiler (Automatic Memoization)
**Revolutionary Change:** React 19's compiler eliminates most manual memoization needs.
**How It Works:**
- Analyzes components at compile-time
- Automatically skips unnecessary re-renders
- Memoizes expensive calculations
- Stabilizes function references
**What This Means:**
```javascript
// React 18 - Manual memoization needed
const ExpensiveComponent = memo(({ data }) => {
const processed = useMemo(() => processData(data), [data]);
const handleClick = useCallback(() => { /* ... */ }, []);
return
{processed}
;
});
// React 19 - Compiler handles it automatically
function ExpensiveComponent({ data }) {
const processed = processData(data); // Auto-memoized by compiler
const handleClick = () => { /* ... */ }; // Auto-stabilized
return
{processed}
;
}
```
**When Manual Memoization Is Still Needed:**
- Third-party libraries requiring memoized values
- Passing functions to `React.memo` components with strict equality
- Extreme performance-critical sections
### 2. Concurrent Rendering Features
React 19 enables concurrent rendering by default, allowing interruptible rendering.
#### `useTransition`
Mark non-urgent updates to keep UI responsive:
```javascript
function SearchPage() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const [results, setResults] = useState([]);
function handleChange(e) {
const value = e.target.value;
setQuery(value); // Urgent: update input immediately
startTransition(() => {
// Non-urgent: search can be interrupted
setResults(searchData(value));
});
}
return (
);
}
```
### 3. Suspense for Data Fetching
Use Suspense to handle loading states declaratively:
```javascript
import { use, Suspense } from 'react';
function UserProfile({ userPromise }) {
const user = use(userPromise); // Suspends until loaded
return
{user.name}
;
}
function ProfilePage({ userId }) {
const userPromise = fetchUser(userId); // Don't await here
return (
}>
);
}
```
**Suspense Batching (React 19.2):**
React 19.2 introduced Suspense batching for server-side rendering, dramatically improving performance by grouping multiple Suspense boundaries.
### 4. Lazy Loading and Code Splitting
Split large components to reduce initial bundle:
```javascript
import { lazy, Suspense } from 'react';
const AdminPanel = lazy(() => import('./AdminPanel'));
const Dashboard = lazy(() => import('./Dashboard'));
function App() {
const { isAdmin } = useAuth();
return (
}>
{isAdmin ? : }
);
}
```
### 5. Virtualization for Long Lists
Don't render thousands of items at once:
```javascript
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }) {
const parentRef = useRef();
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
});
return (