Report Date: November 30, 2025 Research Focus: React 19 development best practices, new features, patterns, and security
Table of Contents #
- React 19 Overview
- New Features and Migration Patterns
- Component Patterns and Hooks Best Practices
- Performance Optimization
- TypeScript Integration Patterns
- Testing React Components
- Security Considerations
- 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
ifstatements and loops (unlike traditional hooks) - Must be used with Suspense for Promise handling
Example with Promises:
1import { use, Suspense } from 'react';
2
3function DataComponent({ dataPromise }) {
4 const data = use(dataPromise); // Suspends until promise resolves
5 return <div>{data}</div>;
6}
7
8// Usage with Suspense
9<Suspense fallback={<p>Loading...</p>}>
10 <DataComponent dataPromise={fetchData()} />
11</Suspense>
Example with Context (Conditional):
1function MyComponent({ isSpecialMode }) {
2 if (isSpecialMode) {
3 const theme = use(ThemeContext); // Can use conditionally!
4 return <div className={`theme-${theme}`}>Special Mode</div>;
5 }
6 return <div>Normal Mode</div>;
7}
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:
1const [state, formAction] = useActionState(asyncFunction, initialState);
Parameters:
asyncFunction: Receives current state and FormDatainitialState: Initial state value
Example:
1async function submitUser(prevState, formData) {
2 try {
3 const username = formData.get('username');
4 const newUser = await api.createUser(username);
5 return {
6 users: [...prevState.users, newUser],
7 error: null
8 };
9 } catch (error) {
10 return {
11 ...prevState,
12 error: error.message
13 };
14 }
15}
16
17function UserForm() {
18 const [state, formAction] = useActionState(submitUser, {
19 users: [],
20 error: null
21 });
22
23 return (
24 <form action={formAction}>
25 <input type="text" name="username" required />
26 <button type="submit">Add User</button>
27 {state?.error && <div className="error">{state.error}</div>}
28 <ul>
29 {state?.users?.map(user => (
30 <li key={user.id}>{user.username}</li>
31 ))}
32 </ul>
33 </form>
34 );
35}
useOptimistic #
Enables optimistic UI updates that show expected results immediately while the server confirms.
Syntax:
1const [optimisticState, setOptimisticState] = useOptimistic(actualState, updateFn);
Behavior:
- Displays optimistic value immediately
- Automatically reverts if operation fails
- Updates to actual value on success
Example:
1function TodoList({ todos }) {
2 const [optimisticTodos, addOptimisticTodo] = useOptimistic(
3 todos,
4 (state, newTodo) => [...state, { ...newTodo, sending: true }]
5 );
6
7 async function handleSubmit(formData) {
8 const title = formData.get('title');
9 addOptimisticTodo({ id: Date.now(), title });
10
11 try {
12 await api.createTodo(title);
13 // Success - optimistic update becomes permanent
14 } catch (error) {
15 // Failure - automatically reverts to original state
16 console.error('Failed to create todo:', error);
17 }
18 }
19
20 return (
21 <>
22 <form action={handleSubmit}>
23 <input name="title" placeholder="New todo" />
24 <button type="submit">Add</button>
25 </form>
26 <ul>
27 {optimisticTodos.map(todo => (
28 <li key={todo.id} style={{ opacity: todo.sending ? 0.5 : 1 }}>
29 {todo.title}
30 </li>
31 ))}
32 </ul>
33 </>
34 );
35}
useFormStatus #
Reads parent form status without prop drilling, similar to Context but specialized for forms.
Important: Must be imported from react-dom, not react:
1import { useFormStatus } from 'react-dom';
Returns: { pending, data } where:
pending: Boolean indicating if form is submittingdata: FormData object with form values
Example:
1function SubmitButton() {
2 const { pending, data } = useFormStatus();
3
4 return (
5 <div>
6 <button disabled={pending} type="submit">
7 {pending ? 'Submitting...' : 'Submit'}
8 </button>
9 {pending && data && (
10 <p>Submitting {data.get('username')}...</p>
11 )}
12 </div>
13 );
14}
15
16function Form() {
17 return (
18 <form action={submitAction}>
19 <input type="text" name="username" placeholder="Enter name" />
20 <SubmitButton /> {/* Accesses form status without props */}
21 </form>
22 );
23}
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:
1async function createPost(formData) {
2 'use server'; // Server Action
3
4 const title = formData.get('title');
5 const post = await db.posts.create({ title });
6 revalidatePath('/posts');
7 return post;
8}
9
10function PostForm() {
11 return (
12 <form action={createPost}>
13 <input name="title" required />
14 <button type="submit">Create Post</button>
15 </form>
16 );
17}
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:
1// app/posts/actions.js
2'use server';
3
4export async function getPosts() {
5 const posts = await db.posts.findMany();
6 return posts;
7}
8
9export async function createPost(formData) {
10 const title = formData.get('title');
11 await db.posts.create({ title });
12 revalidatePath('/posts');
13}
14
15// app/posts/page.js (Server Component)
16import { getPosts } from './actions';
17
18export default async function PostsPage() {
19 const posts = await getPosts(); // Direct async/await
20
21 return (
22 <div>
23 <h1>Posts</h1>
24 <ul>
25 {posts.map(post => (
26 <li key={post.id}>{post.title}</li>
27 ))}
28 </ul>
29 </div>
30 );
31}
4. Developer Experience Improvements #
ref as Prop (No More forwardRef) #
Function components now accept ref directly:
1// React 19 - Simple and clean
2function Input({ ref, ...props }) {
3 return <input ref={ref} {...props} />;
4}
5
6// React 18 - Verbose
7const Input = forwardRef((props, ref) => {
8 return <input ref={ref} {...props} />;
9});
Simplified Context Provider #
1// React 19
2<ThemeContext value="dark">
3 <App />
4</ThemeContext>
5
6// React 18
7<ThemeContext.Provider value="dark">
8 <App />
9</ThemeContext.Provider>
Ref Cleanup Functions #
1function Component() {
2 return (
3 <div ref={element => {
4 // Setup
5 element.addEventListener('scroll', handleScroll);
6
7 // Cleanup (new in React 19)
8 return () => {
9 element.removeEventListener('scroll', handleScroll);
10 };
11 }}>
12 Content
13 </div>
14 );
15}
Enhanced useDeferredValue #
1function SearchResults({ query }) {
2 const deferredQuery = useDeferredValue(query, {
3 initialValue: '' // Show empty initially, update in background
4 });
5
6 const results = search(deferredQuery);
7 return <ResultsList results={results} />;
8}
5. Document Metadata Support #
No more react-helmet needed:
1function BlogPost({ post }) {
2 return (
3 <article>
4 <title>{post.title} - My Blog</title>
5 <meta name="description" content={post.excerpt} />
6 <meta property="og:image" content={post.image} />
7 <link rel="canonical" href={`https://myblog.com/posts/${post.slug}`} />
8
9 <h1>{post.title}</h1>
10 <p>{post.content}</p>
11 </article>
12 );
13}
6. Resource Preloading APIs #
New APIs for performance optimization:
1import { preload, preinit, preconnect, prefetchDNS } from 'react-dom';
2
3// Preload a resource
4preload('/assets/font.woff2', { as: 'font', type: 'font/woff2' });
5
6// Preinit (preload + execute)
7preinit('/scripts/analytics.js', { as: 'script' });
8
9// Preconnect to external domain
10preconnect('https://cdn.example.com');
11
12// DNS prefetch
13prefetchDNS('https://api.example.com');
Migration Patterns #
Breaking Changes to Address #
-
PropTypes Removed
- Migrate to TypeScript or remove PropTypes
- Function component
defaultPropsremoved (use ES6 defaults)
-
Legacy Context Removed
- Migrate to modern
createContextAPI - Class components: use
contextType
- Migrate to modern
-
String Refs Removed
- Replace with callback refs:
ref={el => this.input = el}
- Replace with callback refs:
-
ReactDOM Methods Removed
ReactDOM.render()→createRoot().render()ReactDOM.hydrate()→hydrateRoot()unmountComponentAtNode()→root.unmount()findDOMNode()→ Use refs instead
-
Test Utilities Removed
react-test-renderer/shallow→ Use React Testing Libraryreact-dom/test-utils→ Use React Testing Library
Automated Migration #
Use codemods for automatic migration:
1# Comprehensive migration recipe
2npx codemod@latest react/19/migration-recipe
3
4# Or specific codemods
5npx react-codemod replace-reactdom-render
6npx react-codemod replace-string-ref
Incremental Adoption Strategy #
-
Update Dependencies:
1npm install --save-exact react@^19.0.0 react-dom@^19.0.0 2npm install --save-exact @types/react@^19.0.0 @types/react-dom@^19.0.0 -
Test in Staging:
- Evaluate compatibility with your stack
- Run automated tests
- Test critical user flows
-
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
-
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:
1// Good - Modern functional component
2function UserProfile({ userId }) {
3 const [user, setUser] = useState(null);
4 const [loading, setLoading] = useState(true);
5
6 useEffect(() => {
7 fetchUser(userId).then(setUser).finally(() => setLoading(false));
8 }, [userId]);
9
10 if (loading) return <Spinner />;
11 return <div>{user.name}</div>;
12}
13
14// Avoid - Class component (legacy)
15class UserProfile extends React.Component {
16 // ...outdated pattern
17}
2. Custom Hooks for Reusable Logic #
Custom hooks are the preferred way to share stateful logic across components.
Best Practice:
1// Custom hook for data fetching
2function useUser(userId) {
3 const [user, setUser] = useState(null);
4 const [loading, setLoading] = useState(true);
5 const [error, setError] = useState(null);
6
7 useEffect(() => {
8 let cancelled = false;
9
10 setLoading(true);
11 fetchUser(userId)
12 .then(data => {
13 if (!cancelled) {
14 setUser(data);
15 setError(null);
16 }
17 })
18 .catch(err => {
19 if (!cancelled) setError(err);
20 })
21 .finally(() => {
22 if (!cancelled) setLoading(false);
23 });
24
25 return () => { cancelled = true; };
26 }, [userId]);
27
28 return { user, loading, error };
29}
30
31// Usage in multiple components
32function UserProfile({ userId }) {
33 const { user, loading, error } = useUser(userId);
34
35 if (loading) return <Spinner />;
36 if (error) return <Error message={error.message} />;
37 return <div>{user.name}</div>;
38}
39
40function UserAvatar({ userId }) {
41 const { user, loading } = useUser(userId);
42
43 if (loading) return <SkeletonAvatar />;
44 return <img src={user.avatar} alt={user.name} />;
45}
3. Hooks Composition Pattern #
Combine multiple custom hooks for complex behavior:
1function useAuthenticatedUser() {
2 const { user, loading: authLoading } = useAuth();
3 const { data: profile, loading: profileLoading } = useUserProfile(user?.id);
4 const { permissions, loading: permLoading } = usePermissions(user?.id);
5
6 return {
7 user,
8 profile,
9 permissions,
10 loading: authLoading || profileLoading || permLoading
11 };
12}
13
14// Clean component using composed hook
15function Dashboard() {
16 const { user, profile, permissions, loading } = useAuthenticatedUser();
17
18 if (loading) return <Spinner />;
19
20 return (
21 <div>
22 <h1>Welcome, {profile.name}</h1>
23 {permissions.canEdit && <EditButton />}
24 </div>
25 );
26}
4. State Reducer Pattern #
For complex state logic, use the reducer pattern:
1const initialState = {
2 items: [],
3 filter: 'all',
4 sortBy: 'date',
5 loading: false,
6 error: null
7};
8
9function listReducer(state, action) {
10 switch (action.type) {
11 case 'FETCH_START':
12 return { ...state, loading: true, error: null };
13 case 'FETCH_SUCCESS':
14 return { ...state, loading: false, items: action.payload };
15 case 'FETCH_ERROR':
16 return { ...state, loading: false, error: action.payload };
17 case 'SET_FILTER':
18 return { ...state, filter: action.payload };
19 case 'SET_SORT':
20 return { ...state, sortBy: action.payload };
21 default:
22 return state;
23 }
24}
25
26function ItemList() {
27 const [state, dispatch] = useReducer(listReducer, initialState);
28
29 useEffect(() => {
30 dispatch({ type: 'FETCH_START' });
31 fetchItems()
32 .then(items => dispatch({ type: 'FETCH_SUCCESS', payload: items }))
33 .catch(error => dispatch({ type: 'FETCH_ERROR', payload: error }));
34 }, []);
35
36 const filteredItems = filterAndSort(state.items, state.filter, state.sortBy);
37
38 return (
39 <div>
40 <FilterControls
41 filter={state.filter}
42 onFilterChange={f => dispatch({ type: 'SET_FILTER', payload: f })}
43 />
44 <ItemGrid items={filteredItems} />
45 </div>
46 );
47}
5. Component Composition over Props Drilling #
Use composition to avoid deep prop drilling:
1// Good - Composition
2function Dashboard({ children }) {
3 const user = useUser();
4
5 return (
6 <div className="dashboard">
7 <Sidebar user={user} />
8 <main>{children}</main>
9 </div>
10 );
11}
12
13function App() {
14 return (
15 <Dashboard>
16 <UserProfile /> {/* Gets user from context, not props */}
17 <ActivityFeed />
18 </Dashboard>
19 );
20}
21
22// Avoid - Props drilling
23function Dashboard({ user, children }) {
24 return (
25 <div className="dashboard">
26 <Sidebar user={user} />
27 <main>{React.cloneElement(children, { user })}</main>
28 </div>
29 );
30}
6. Avoid Unnecessary useEffect #
React 19 emphasizes reducing useEffect usage:
1// Bad - Unnecessary effect
2function SearchResults({ query }) {
3 const [results, setResults] = useState([]);
4
5 useEffect(() => {
6 setResults(filterResults(data, query));
7 }, [query, data]);
8
9 return <ResultsList results={results} />;
10}
11
12// Good - Direct calculation
13function SearchResults({ query }) {
14 const results = useMemo(() => filterResults(data, query), [query, data]);
15 return <ResultsList results={results} />;
16}
17
18// Better - May not even need useMemo if fast
19function SearchResults({ query }) {
20 const results = filterResults(data, query); // React Compiler handles this
21 return <ResultsList results={results} />;
22}
7. Rules of Hooks (Still Apply) #
- Only call hooks at the top level - No conditionals, loops, or nested functions
- Only call hooks from React functions - Components or custom hooks
- Custom hooks must start with "use" - Naming convention for tooling
1// Bad - Conditional hook
2function Component({ shouldFetch }) {
3 if (shouldFetch) {
4 const data = useFetch('/api/data'); // ERROR!
5 }
6}
7
8// Good - Conditional logic inside hook
9function Component({ shouldFetch }) {
10 const data = useFetch(shouldFetch ? '/api/data' : null);
11}
12
13function useFetch(url) {
14 const [data, setData] = useState(null);
15
16 useEffect(() => {
17 if (!url) return;
18 fetch(url).then(r => r.json()).then(setData);
19 }, [url]);
20
21 return data;
22}
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:
1// React 18 - Manual memoization needed
2const ExpensiveComponent = memo(({ data }) => {
3 const processed = useMemo(() => processData(data), [data]);
4 const handleClick = useCallback(() => { /* ... */ }, []);
5
6 return <div onClick={handleClick}>{processed}</div>;
7});
8
9// React 19 - Compiler handles it automatically
10function ExpensiveComponent({ data }) {
11 const processed = processData(data); // Auto-memoized by compiler
12 const handleClick = () => { /* ... */ }; // Auto-stabilized
13
14 return <div onClick={handleClick}>{processed}</div>;
15}
When Manual Memoization Is Still Needed:
- Third-party libraries requiring memoized values
- Passing functions to
React.memocomponents 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:
1function SearchPage() {
2 const [query, setQuery] = useState('');
3 const [isPending, startTransition] = useTransition();
4 const [results, setResults] = useState([]);
5
6 function handleChange(e) {
7 const value = e.target.value;
8 setQuery(value); // Urgent: update input immediately
9
10 startTransition(() => {
11 // Non-urgent: search can be interrupted
12 setResults(searchData(value));
13 });
14 }
15
16 return (
17 <div>
18 <input value={query} onChange={handleChange} />
19 {isPending && <Spinner />}
20 <ResultsList results={results} />
21 </div>
22 );
23}
useDeferredValue #
Defer expensive updates:
1function FilteredList({ items, filter }) {
2 const deferredFilter = useDeferredValue(filter);
3 const filtered = items.filter(item => item.includes(deferredFilter));
4
5 return (
6 <div>
7 {filter !== deferredFilter && <Spinner />}
8 <ul>
9 {filtered.map(item => <li key={item}>{item}</li>)}
10 </ul>
11 </div>
12 );
13}
3. Suspense for Data Fetching #
Use Suspense to handle loading states declaratively:
1import { use, Suspense } from 'react';
2
3function UserProfile({ userPromise }) {
4 const user = use(userPromise); // Suspends until loaded
5 return <div>{user.name}</div>;
6}
7
8function ProfilePage({ userId }) {
9 const userPromise = fetchUser(userId); // Don't await here
10
11 return (
12 <Suspense fallback={<ProfileSkeleton />}>
13 <UserProfile userPromise={userPromise} />
14 </Suspense>
15 );
16}
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:
1import { lazy, Suspense } from 'react';
2
3const AdminPanel = lazy(() => import('./AdminPanel'));
4const Dashboard = lazy(() => import('./Dashboard'));
5
6function App() {
7 const { isAdmin } = useAuth();
8
9 return (
10 <Suspense fallback={<PageSpinner />}>
11 {isAdmin ? <AdminPanel /> : <Dashboard />}
12 </Suspense>
13 );
14}
5. Virtualization for Long Lists #
Don't render thousands of items at once:
1import { useVirtualizer } from '@tanstack/react-virtual';
2
3function VirtualList({ items }) {
4 const parentRef = useRef();
5
6 const virtualizer = useVirtualizer({
7 count: items.length,
8 getScrollElement: () => parentRef.current,
9 estimateSize: () => 50,
10 });
11
12 return (
13 <div ref={parentRef} style={{ height: '500px', overflow: 'auto' }}>
14 <div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
15 {virtualizer.getVirtualItems().map(virtualItem => (
16 <div
17 key={virtualItem.key}
18 style={{
19 position: 'absolute',
20 top: 0,
21 left: 0,
22 width: '100%',
23 height: `${virtualItem.size}px`,
24 transform: `translateY(${virtualItem.start}px)`,
25 }}
26 >
27 {items[virtualItem.index]}
28 </div>
29 ))}
30 </div>
31 </div>
32 );
33}
6. Image Optimization #
Modern image optimization techniques:
1function OptimizedImage({ src, alt }) {
2 return (
3 <picture>
4 <source
5 type="image/webp"
6 srcSet={`${src}.webp 1x, ${src}@2x.webp 2x`}
7 />
8 <img
9 src={src}
10 alt={alt}
11 loading="lazy"
12 decoding="async"
13 width={800}
14 height={600}
15 />
16 </picture>
17 );
18}
7. Resource Preloading #
Use React 19's new preloading APIs:
1import { preload, prefetchDNS } from 'react-dom';
2
3function ProductPage({ productId }) {
4 // Preload product image as soon as component mounts
5 useEffect(() => {
6 preload(`/images/product-${productId}.jpg`, { as: 'image' });
7 prefetchDNS('https://api.example.com');
8 }, [productId]);
9
10 return <Product id={productId} />;
11}
8. Profiling and Monitoring #
Continuous Performance Monitoring:
1// Use React DevTools Profiler
2import { Profiler } from 'react';
3
4function onRenderCallback(
5 id, // component identifier
6 phase, // "mount" or "update"
7 actualDuration, // time spent rendering
8 baseDuration, // estimated time without memoization
9 startTime,
10 commitTime,
11 interactions
12) {
13 // Log or send to analytics
14 analytics.track('component-render', {
15 id,
16 phase,
17 duration: actualDuration
18 });
19}
20
21function App() {
22 return (
23 <Profiler id="App" onRender={onRenderCallback}>
24 <Dashboard />
25 </Profiler>
26 );
27}
Key Recommendations:
- Profile regularly during development
- Set up Real User Monitoring (RUM) for production
- Monitor Core Web Vitals (LCP, FID, CLS)
- Use React DevTools Profiler to identify bottlenecks
- Don't optimize prematurely - measure first
TypeScript Integration Patterns #
1. Component Props Typing #
Function Components:
1interface UserCardProps {
2 name: string;
3 age: number;
4 email?: string;
5 onEdit?: () => void;
6}
7
8function UserCard({ name, age, email, onEdit }: UserCardProps) {
9 return (
10 <div>
11 <h2>{name}</h2>
12 <p>Age: {age}</p>
13 {email && <p>Email: {email}</p>}
14 {onEdit && <button onClick={onEdit}>Edit</button>}
15 </div>
16 );
17}
Extending HTML Element Props:
1import { ComponentProps } from 'react';
2
3// Inherit all button props
4interface CustomButtonProps extends ComponentProps<'button'> {
5 variant?: 'primary' | 'secondary';
6 loading?: boolean;
7}
8
9function CustomButton({ variant = 'primary', loading, children, ...props }: CustomButtonProps) {
10 return (
11 <button
12 {...props}
13 className={`btn btn-${variant}`}
14 disabled={loading || props.disabled}
15 >
16 {loading ? 'Loading...' : children}
17 </button>
18 );
19}
Using ComponentPropsWithRef (React 19):
1import { ComponentPropsWithRef, forwardRef } from 'react';
2
3// React 19 - No forwardRef needed
4interface InputProps extends ComponentPropsWithRef<'input'> {
5 label: string;
6}
7
8function Input({ label, ref, ...props }: InputProps) {
9 return (
10 <div>
11 <label>{label}</label>
12 <input ref={ref} {...props} />
13 </div>
14 );
15}
2. Event Handler Typing #
1// Specific event types
2function Form() {
3 const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
4 e.preventDefault();
5 // Handle submission
6 };
7
8 const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
9 console.log(e.target.value);
10 };
11
12 const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
13 if (e.key === 'Enter') {
14 // Handle enter
15 }
16 };
17
18 return (
19 <form onSubmit={handleSubmit}>
20 <input onChange={handleInputChange} onKeyDown={handleKeyDown} />
21 </form>
22 );
23}
24
25// Generic handler type
26type ClickHandler = React.MouseEventHandler<HTMLButtonElement>;
27
28const handleClick: ClickHandler = (e) => {
29 console.log(e.currentTarget); // Typed as HTMLButtonElement
30};
Using Handler Types:
1interface ButtonProps {
2 onClick?: React.MouseEventHandler<HTMLButtonElement>;
3 onFocus?: React.FocusEventHandler<HTMLButtonElement>;
4}
3. State and Refs Typing #
1// State with explicit type
2const [user, setUser] = useState<User | null>(null);
3
4// State with initial value (type inferred)
5const [count, setCount] = useState(0); // Type: number
6
7// Array state
8const [items, setItems] = useState<Item[]>([]);
9
10// Complex state
11interface FormState {
12 values: Record<string, string>;
13 errors: Record<string, string>;
14 isSubmitting: boolean;
15}
16
17const [formState, setFormState] = useState<FormState>({
18 values: {},
19 errors: {},
20 isSubmitting: false
21});
22
23// Refs (React 19 requires initial value)
24const inputRef = useRef<HTMLInputElement>(null);
25
26// Ref for mutable value
27const timerRef = useRef<NodeJS.Timeout | null>(null);
28
29useEffect(() => {
30 timerRef.current = setTimeout(() => {
31 // Do something
32 }, 1000);
33
34 return () => {
35 if (timerRef.current) {
36 clearTimeout(timerRef.current);
37 }
38 };
39}, []);
4. Custom Hooks Typing #
1interface UseUserResult {
2 user: User | null;
3 loading: boolean;
4 error: Error | null;
5 refetch: () => Promise<void>;
6}
7
8function useUser(userId: string): UseUserResult {
9 const [user, setUser] = useState<User | null>(null);
10 const [loading, setLoading] = useState(true);
11 const [error, setError] = useState<Error | null>(null);
12
13 const fetchUser = useCallback(async () => {
14 try {
15 setLoading(true);
16 const data = await api.fetchUser(userId);
17 setUser(data);
18 setError(null);
19 } catch (err) {
20 setError(err as Error);
21 } finally {
22 setLoading(false);
23 }
24 }, [userId]);
25
26 useEffect(() => {
27 fetchUser();
28 }, [fetchUser]);
29
30 return { user, loading, error, refetch: fetchUser };
31}
5. Generic Components #
1interface ListProps<T> {
2 items: T[];
3 renderItem: (item: T, index: number) => React.ReactNode;
4 keyExtractor: (item: T) => string | number;
5}
6
7function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
8 return (
9 <ul>
10 {items.map((item, index) => (
11 <li key={keyExtractor(item)}>
12 {renderItem(item, index)}
13 </li>
14 ))}
15 </ul>
16 );
17}
18
19// Usage with type inference
20function UserList() {
21 const users: User[] = [...];
22
23 return (
24 <List
25 items={users}
26 renderItem={(user) => <UserCard user={user} />} // user is typed as User
27 keyExtractor={(user) => user.id}
28 />
29 );
30}
6. Context Typing #
1interface ThemeContextValue {
2 theme: 'light' | 'dark';
3 toggleTheme: () => void;
4}
5
6const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
7
8function ThemeProvider({ children }: { children: React.ReactNode }) {
9 const [theme, setTheme] = useState<'light' | 'dark'>('light');
10
11 const toggleTheme = useCallback(() => {
12 setTheme(prev => prev === 'light' ? 'dark' : 'light');
13 }, []);
14
15 const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme]);
16
17 return (
18 <ThemeContext value={value}>
19 {children}
20 </ThemeContext>
21 );
22}
23
24// Custom hook for type-safe context consumption
25function useTheme(): ThemeContextValue {
26 const context = use(ThemeContext);
27 if (!context) {
28 throw new Error('useTheme must be used within ThemeProvider');
29 }
30 return context;
31}
7. Children and ReactNode #
1// Basic children
2interface CardProps {
3 children: React.ReactNode;
4 title?: string;
5}
6
7// Render prop pattern
8interface RenderPropProps<T> {
9 data: T[];
10 render: (item: T) => React.ReactNode;
11}
12
13// Function as children
14interface LoadingWrapperProps {
15 loading: boolean;
16 children: (data: Data) => React.ReactNode;
17}
18
19function LoadingWrapper({ loading, children }: LoadingWrapperProps) {
20 const data = useData();
21
22 if (loading) return <Spinner />;
23 return <>{children(data)}</>;
24}
8. React 19 Specific Types #
1// useActionState typing
2type ActionState = {
3 errors?: Record<string, string>;
4 success?: boolean;
5};
6
7async function submitAction(
8 prevState: ActionState,
9 formData: FormData
10): Promise<ActionState> {
11 // Implementation
12 return { success: true };
13}
14
15function Form() {
16 const [state, action] = useActionState(submitAction, {});
17
18 return (
19 <form action={action}>
20 {/* Form fields */}
21 </form>
22 );
23}
24
25// useOptimistic typing
26interface Todo {
27 id: number;
28 title: string;
29 sending?: boolean;
30}
31
32function TodoList({ todos }: { todos: Todo[] }) {
33 const [optimisticTodos, addOptimisticTodo] = useOptimistic(
34 todos,
35 (state: Todo[], newTodo: Todo): Todo[] => [...state, newTodo]
36 );
37
38 return <ul>{/* Render todos */}</ul>;
39}
TypeScript Configuration #
Recommended tsconfig.json:
1{
2 "compilerOptions": {
3 "target": "ES2020",
4 "lib": ["ES2020", "DOM", "DOM.Iterable"],
5 "jsx": "react-jsx",
6 "module": "ESNext",
7 "moduleResolution": "bundler",
8 "strict": true,
9 "noUnusedLocals": true,
10 "noUnusedParameters": true,
11 "noFallthroughCasesInSwitch": true,
12 "allowSyntheticDefaultImports": true,
13 "esModuleInterop": true,
14 "skipLibCheck": true,
15 "forceConsistentCasingInFileNames": true,
16 "resolveJsonModule": true,
17 "isolatedModules": true,
18 "paths": {
19 "@/*": ["./src/*"]
20 }
21 },
22 "include": ["src"],
23 "exclude": ["node_modules", "dist"]
24}
Testing React Components #
1. Vitest + React Testing Library Setup #
Why Vitest?
- Lightning-fast execution (uses Vite/esbuild)
- Native ESM and TypeScript support
- No complex configuration needed
- Modern UI dashboard
- Drop-in replacement for Jest
Installation:
1npm install -D vitest jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event @vitest/coverage-v8
Configuration (vite.config.ts):
1import { defineConfig } from 'vitest/config';
2import react from '@vitejs/plugin-react';
3
4export default defineConfig({
5 plugins: [react()],
6 test: {
7 environment: 'jsdom',
8 globals: true,
9 setupFiles: './src/test/setup.ts',
10 coverage: {
11 provider: 'v8',
12 reporter: ['text', 'json', 'html'],
13 exclude: [
14 'node_modules/',
15 'src/test/',
16 ]
17 }
18 }
19});
Setup File (src/test/setup.ts):
1import { cleanup } from '@testing-library/react';
2import { afterEach } from 'vitest';
3import '@testing-library/jest-dom';
4
5// Cleanup after each test
6afterEach(() => {
7 cleanup();
8});
Package Scripts:
1{
2 "scripts": {
3 "test": "vitest",
4 "test:ui": "vitest --ui",
5 "test:coverage": "vitest --coverage"
6 }
7}
2. Testing Best Practices #
Test User Behavior, Not Implementation:
1import { render, screen } from '@testing-library/react';
2import { userEvent } from '@testing-library/user-event';
3import { describe, it, expect, vi } from 'vitest';
4
5// Good - Tests behavior
6describe('LoginForm', () => {
7 it('submits form with username and password', async () => {
8 const user = userEvent.setup();
9 const onSubmit = vi.fn();
10
11 render(<LoginForm onSubmit={onSubmit} />);
12
13 await user.type(screen.getByLabelText(/username/i), 'testuser');
14 await user.type(screen.getByLabelText(/password/i), 'password123');
15 await user.click(screen.getByRole('button', { name: /log in/i }));
16
17 expect(onSubmit).toHaveBeenCalledWith({
18 username: 'testuser',
19 password: 'password123'
20 });
21 });
22
23 it('shows error when username is empty', async () => {
24 const user = userEvent.setup();
25
26 render(<LoginForm onSubmit={vi.fn()} />);
27
28 await user.click(screen.getByRole('button', { name: /log in/i }));
29
30 expect(screen.getByText(/username is required/i)).toBeInTheDocument();
31 });
32});
33
34// Avoid - Tests implementation details
35describe('LoginForm', () => {
36 it('updates state when input changes', () => {
37 const { container } = render(<LoginForm />);
38 const input = container.querySelector('.username-input'); // Bad: implementation detail
39
40 fireEvent.change(input, { target: { value: 'test' } }); // Bad: fireEvent instead of userEvent
41
42 expect(input.value).toBe('test'); // Bad: testing state directly
43 });
44});
3. Testing Async Components #
With Suspense and use():
1import { render, screen, waitFor } from '@testing-library/react';
2import { Suspense } from 'react';
3
4describe('UserProfile', () => {
5 it('displays user data after loading', async () => {
6 const userPromise = Promise.resolve({ name: 'John Doe', email: 'john@example.com' });
7
8 render(
9 <Suspense fallback={<div>Loading...</div>}>
10 <UserProfile userPromise={userPromise} />
11 </Suspense>
12 );
13
14 // Initially shows loading
15 expect(screen.getByText(/loading/i)).toBeInTheDocument();
16
17 // Wait for data to load
18 await waitFor(() => {
19 expect(screen.getByText('John Doe')).toBeInTheDocument();
20 });
21
22 expect(screen.getByText('john@example.com')).toBeInTheDocument();
23 });
24});
Testing useEffect Data Fetching:
1describe('UserList', () => {
2 it('fetches and displays users', async () => {
3 const mockUsers = [
4 { id: 1, name: 'User 1' },
5 { id: 2, name: 'User 2' }
6 ];
7
8 vi.spyOn(api, 'fetchUsers').mockResolvedValue(mockUsers);
9
10 render(<UserList />);
11
12 expect(screen.getByText(/loading/i)).toBeInTheDocument();
13
14 await waitFor(() => {
15 expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
16 });
17
18 expect(screen.getByText('User 1')).toBeInTheDocument();
19 expect(screen.getByText('User 2')).toBeInTheDocument();
20 });
21
22 it('handles fetch errors', async () => {
23 vi.spyOn(api, 'fetchUsers').mockRejectedValue(new Error('Failed to fetch'));
24
25 render(<UserList />);
26
27 await waitFor(() => {
28 expect(screen.getByText(/error/i)).toBeInTheDocument();
29 });
30 });
31});
4. Mocking in Vitest #
Mocking Modules:
1import { vi } from 'vitest';
2
3// Mock entire module
4vi.mock('./api', () => ({
5 fetchUser: vi.fn(),
6 createUser: vi.fn()
7}));
8
9// Mock with factory
10vi.mock('axios', () => ({
11 default: {
12 get: vi.fn(),
13 post: vi.fn()
14 }
15}));
16
17// Use in test
18import { fetchUser } from './api';
19
20describe('Component', () => {
21 beforeEach(() => {
22 vi.clearAllMocks(); // Clear mocks before each test
23 });
24
25 it('fetches user data', async () => {
26 vi.mocked(fetchUser).mockResolvedValue({ name: 'John' });
27
28 render(<Component />);
29
30 await waitFor(() => {
31 expect(fetchUser).toHaveBeenCalledWith('123');
32 });
33 });
34});
Mocking Timers:
1describe('AutoSaveForm', () => {
2 beforeEach(() => {
3 vi.useFakeTimers();
4 });
5
6 afterEach(() => {
7 vi.restoreAllMocks();
8 });
9
10 it('auto-saves after 2 seconds', async () => {
11 const user = userEvent.setup({ delay: null }); // Important for fake timers
12 const onSave = vi.fn();
13
14 render(<AutoSaveForm onSave={onSave} />);
15
16 await user.type(screen.getByRole('textbox'), 'Hello');
17
18 expect(onSave).not.toHaveBeenCalled();
19
20 vi.advanceTimersByTime(2000);
21
22 expect(onSave).toHaveBeenCalledWith('Hello');
23 });
24});
5. Testing Hooks #
Using renderHook from @testing-library/react:
1import { renderHook, waitFor } from '@testing-library/react';
2
3describe('useUser', () => {
4 it('fetches user data', async () => {
5 vi.spyOn(api, 'fetchUser').mockResolvedValue({ name: 'John' });
6
7 const { result } = renderHook(() => useUser('123'));
8
9 expect(result.current.loading).toBe(true);
10 expect(result.current.user).toBe(null);
11
12 await waitFor(() => {
13 expect(result.current.loading).toBe(false);
14 });
15
16 expect(result.current.user).toEqual({ name: 'John' });
17 expect(result.current.error).toBe(null);
18 });
19
20 it('handles errors', async () => {
21 vi.spyOn(api, 'fetchUser').mockRejectedValue(new Error('Failed'));
22
23 const { result } = renderHook(() => useUser('123'));
24
25 await waitFor(() => {
26 expect(result.current.loading).toBe(false);
27 });
28
29 expect(result.current.error).toBeTruthy();
30 expect(result.current.user).toBe(null);
31 });
32});
6. Testing Context #
1import { render, screen } from '@testing-library/react';
2
3function renderWithTheme(ui: React.ReactElement, { theme = 'light' } = {}) {
4 return render(
5 <ThemeProvider initialTheme={theme}>
6 {ui}
7 </ThemeProvider>
8 );
9}
10
11describe('ThemedButton', () => {
12 it('renders with light theme', () => {
13 renderWithTheme(<ThemedButton>Click me</ThemedButton>, { theme: 'light' });
14
15 const button = screen.getByRole('button');
16 expect(button).toHaveClass('btn-light');
17 });
18
19 it('renders with dark theme', () => {
20 renderWithTheme(<ThemedButton>Click me</ThemedButton>, { theme: 'dark' });
21
22 const button = screen.getByRole('button');
23 expect(button).toHaveClass('btn-dark');
24 });
25});
7. Browser Mode (2025 Recommendation) #
Vitest Browser Mode provides the most accurate testing environment:
1// vite.config.ts
2import { defineConfig } from 'vitest/config';
3
4export default defineConfig({
5 test: {
6 browser: {
7 enabled: true,
8 name: 'chromium',
9 provider: 'playwright'
10 }
11 }
12});
Migration from DOM mode:
1// Before (jsdom)
2import { render, screen } from '@testing-library/react';
3
4// After (browser mode)
5import { render, screen } from 'vitest-browser-react';
6
7describe('Component', () => {
8 it('renders correctly', async () => {
9 const { container } = render(<MyComponent />);
10
11 // Use await expect.element() for assertions
12 await expect.element(screen.getByText('Hello')).toBeInTheDocument();
13 });
14});
8. Snapshot Testing #
Use sparingly for stable UI:
1import { render } from '@testing-library/react';
2
3describe('Alert', () => {
4 it('matches snapshot for success variant', () => {
5 const { container } = render(<Alert variant="success">Success!</Alert>);
6 expect(container.firstChild).toMatchSnapshot();
7 });
8});
9. Coverage Best Practices #
1// Exclude test files and types
2/// <reference types="vitest" />
Run with coverage:
1npm run test:coverage
Set coverage thresholds (vite.config.ts):
1export default defineConfig({
2 test: {
3 coverage: {
4 provider: 'v8',
5 reporter: ['text', 'html'],
6 statements: 80,
7 branches: 75,
8 functions: 80,
9 lines: 80
10 }
11 }
12});
Security Considerations #
1. XSS (Cross-Site Scripting) Prevention #
React's Built-in Protection #
React automatically escapes values in JSX:
1// Safe - React escapes the value
2function UserGreeting({ name }) {
3 return <div>Hello, {name}!</div>; // Even if name contains <script>, it's escaped
4}
5
6// Safe - Attributes are escaped too
7function UserProfile({ avatarUrl }) {
8 return <img src={avatarUrl} alt="Avatar" />; // URL is escaped
9}
Dangerous Patterns to Avoid #
dangerouslySetInnerHTML without sanitization:
1// DANGEROUS - Never do this with user input!
2function BlogPost({ content }) {
3 return <div dangerouslySetInnerHTML={{ __html: content }} />;
4}
5
6// SAFE - Sanitize with DOMPurify
7import DOMPurify from 'dompurify';
8
9function BlogPost({ content }) {
10 const sanitized = DOMPurify.sanitize(content, {
11 ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a'],
12 ALLOWED_ATTR: ['href']
13 });
14
15 return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
16}
JavaScript URLs:
1// DANGEROUS - javascript: URLs can execute scripts
2function Link({ url }) {
3 return <a href={url}>Click me</a>;
4}
5
6// SAFE - Validate URL protocol
7function Link({ url }) {
8 const isValidUrl = (url: string) => {
9 try {
10 const parsed = new URL(url);
11 return ['http:', 'https:', 'mailto:'].includes(parsed.protocol);
12 } catch {
13 return false;
14 }
15 };
16
17 if (!isValidUrl(url)) {
18 console.warn('Invalid URL blocked:', url);
19 return <span>Invalid link</span>;
20 }
21
22 return <a href={url}>Click me</a>;
23}
Direct DOM Manipulation:
1// DANGEROUS - Bypasses React's protection
2function Component({ userContent }) {
3 useEffect(() => {
4 document.getElementById('content').innerHTML = userContent; // XSS risk!
5 }, [userContent]);
6
7 return <div id="content"></div>;
8}
9
10// SAFE - Use React's rendering
11function Component({ userContent }) {
12 return <div>{userContent}</div>; // React escapes it
13}
2. Content Security Policy (CSP) #
Implement strict CSP headers:
1<!-- In HTML meta tag or HTTP header -->
2<meta http-equiv="Content-Security-Policy" content="
3 default-src 'self';
4 script-src 'self' 'nonce-{RANDOM_NONCE}';
5 style-src 'self' 'nonce-{RANDOM_NONCE}';
6 img-src 'self' https://cdn.example.com;
7 font-src 'self';
8 connect-src 'self' https://api.example.com;
9 frame-ancestors 'none';
10 base-uri 'self';
11 form-action 'self'
12">
For inline scripts (use nonces):
1// Server-side: Generate nonce per request
2const nonce = crypto.randomBytes(16).toString('base64');
3
4// Pass nonce to React
5function App({ nonce }) {
6 return (
7 <html>
8 <head>
9 <script nonce={nonce} src="/app.js"></script>
10 </head>
11 <body>
12 <div id="root"></div>
13 </body>
14 </html>
15 );
16}
3. Authentication and Authorization #
HTTPOnly Cookies:
1// Server-side: Set HTTPOnly cookie
2response.cookie('session', token, {
3 httpOnly: true, // Prevents JavaScript access
4 secure: true, // HTTPS only
5 sameSite: 'strict', // CSRF protection
6 maxAge: 3600000 // 1 hour
7});
Secure Token Storage:
1// BAD - localStorage is accessible to JavaScript (XSS risk)
2localStorage.setItem('authToken', token);
3
4// GOOD - Use HTTPOnly cookies (set by server)
5// Or if you must use client-side storage:
6// 1. Store in memory (lost on refresh)
7const [authToken, setAuthToken] = useState<string | null>(null);
8
9// 2. Use sessionStorage (cleared on tab close)
10sessionStorage.setItem('tempToken', token);
11
12// 3. Best: HTTPOnly cookies managed by server
Protected Routes:
1function ProtectedRoute({ children }: { children: React.ReactNode }) {
2 const { user, loading } = useAuth();
3
4 if (loading) return <Spinner />;
5
6 if (!user) {
7 return <Navigate to="/login" replace />;
8 }
9
10 return <>{children}</>;
11}
12
13function App() {
14 return (
15 <Routes>
16 <Route path="/login" element={<Login />} />
17 <Route path="/dashboard" element={
18 <ProtectedRoute>
19 <Dashboard />
20 </ProtectedRoute>
21 } />
22 </Routes>
23 );
24}
4. Dependency Security #
Regular Updates:
1# Check for vulnerabilities
2npm audit
3
4# Fix vulnerabilities automatically
5npm audit fix
6
7# For breaking changes
8npm audit fix --force
Use Dependabot or Renovate:
1# .github/dependabot.yml
2version: 2
3updates:
4 - package-ecosystem: npm
5 directory: /
6 schedule:
7 interval: weekly
8 open-pull-requests-limit: 10
Verify Third-Party Libraries:
1# Check package reputation
2npm view react-some-library
3
4# Check for known vulnerabilities
5npx snyk test
6
7# Use npm provenance
8npm install --provenance
5. API Security #
Environment Variables:
1// NEVER commit secrets to version control
2// .env (gitignored)
3VITE_API_URL=https://api.example.com
4API_SECRET_KEY=secret123 // Server-side only!
5
6// Client-side (Vite)
7const apiUrl = import.meta.env.VITE_API_URL; // OK - public
8// import.meta.env.API_SECRET_KEY is undefined in client
9
10// Validate env vars at startup
11if (!import.meta.env.VITE_API_URL) {
12 throw new Error('VITE_API_URL is required');
13}
Secure API Calls:
1// Add authentication
2async function fetchData(endpoint: string) {
3 const response = await fetch(`${API_URL}${endpoint}`, {
4 credentials: 'include', // Send cookies
5 headers: {
6 'Content-Type': 'application/json',
7 // Don't put sensitive tokens in headers client-side
8 // Use HTTPOnly cookies instead
9 }
10 });
11
12 if (!response.ok) {
13 throw new Error(`API error: ${response.status}`);
14 }
15
16 return response.json();
17}
Rate Limiting (Client-side):
1// Prevent API abuse
2function useThrottledFetch<T>(fetchFn: () => Promise<T>, delay: number = 1000) {
3 const lastCall = useRef<number>(0);
4
5 return useCallback(async () => {
6 const now = Date.now();
7 if (now - lastCall.current < delay) {
8 throw new Error('Too many requests');
9 }
10
11 lastCall.current = now;
12 return fetchFn();
13 }, [fetchFn, delay]);
14}
6. File Upload Security #
1function FileUpload() {
2 const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
3 const file = e.target.files?.[0];
4
5 if (!file) return;
6
7 // 1. Validate file type
8 const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
9 if (!allowedTypes.includes(file.type)) {
10 alert('Invalid file type');
11 return;
12 }
13
14 // 2. Validate file size (5MB max)
15 const maxSize = 5 * 1024 * 1024;
16 if (file.size > maxSize) {
17 alert('File too large');
18 return;
19 }
20
21 // 3. Validate file name (prevent path traversal)
22 const safeName = file.name.replace(/[^a-zA-Z0-9.-]/g, '_');
23
24 // 4. Upload with proper content-type
25 const formData = new FormData();
26 formData.append('file', file, safeName);
27
28 uploadFile(formData);
29 };
30
31 return (
32 <input
33 type="file"
34 accept="image/jpeg,image/png,image/webp"
35 onChange={handleFileChange}
36 />
37 );
38}
7. Prevent Clickjacking #
1// Server-side: Set X-Frame-Options header
2response.setHeader('X-Frame-Options', 'DENY');
3// Or
4response.setHeader('X-Frame-Options', 'SAMEORIGIN');
5
6// Or use CSP
7response.setHeader('Content-Security-Policy', "frame-ancestors 'none'");
8. Input Validation #
1// Client-side validation (UX) + Server-side validation (security)
2function SignupForm() {
3 const [errors, setErrors] = useState<Record<string, string>>({});
4
5 const validateEmail = (email: string) => {
6 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
7 return emailRegex.test(email);
8 };
9
10 const validatePassword = (password: string) => {
11 // At least 8 chars, 1 uppercase, 1 lowercase, 1 number
12 const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;
13 return passwordRegex.test(password);
14 };
15
16 const handleSubmit = async (e: React.FormEvent) => {
17 e.preventDefault();
18 const formData = new FormData(e.target as HTMLFormElement);
19 const email = formData.get('email') as string;
20 const password = formData.get('password') as string;
21
22 const newErrors: Record<string, string> = {};
23
24 if (!validateEmail(email)) {
25 newErrors.email = 'Invalid email address';
26 }
27
28 if (!validatePassword(password)) {
29 newErrors.password = 'Password must be at least 8 characters with uppercase, lowercase, and number';
30 }
31
32 if (Object.keys(newErrors).length > 0) {
33 setErrors(newErrors);
34 return;
35 }
36
37 // Submit to server (which validates again!)
38 try {
39 await api.signup({ email, password });
40 } catch (error) {
41 // Handle server validation errors
42 if (error.response?.data?.errors) {
43 setErrors(error.response.data.errors);
44 }
45 }
46 };
47
48 return (
49 <form onSubmit={handleSubmit}>
50 <input name="email" type="email" required />
51 {errors.email && <span>{errors.email}</span>}
52
53 <input name="password" type="password" required />
54 {errors.password && <span>{errors.password}</span>}
55
56 <button type="submit">Sign Up</button>
57 </form>
58 );
59}
9. Secure Defaults Checklist #
- Use HTTPS in production
- Set secure HTTP headers (CSP, X-Frame-Options, HSTS)
- Use HTTPOnly cookies for authentication
- Validate all user inputs (client + server)
- Sanitize HTML before using
dangerouslySetInnerHTML - Validate URLs before rendering links
- Keep dependencies updated (npm audit)
- Use environment variables for configuration
- Implement proper error handling (don't leak sensitive info)
- Enable CORS only for trusted domains
- Use rate limiting for API endpoints
- Log security events for monitoring
- Implement proper access control
- Use security linters (ESLint security plugins)
Recommendations Summary #
Immediate Actions for Existing Projects #
-
Upgrade to React 19:
1npx codemod@latest react/19/migration-recipe 2npm install react@^19.0.0 react-dom@^19.0.0 -
Enable React Compiler:
- Reduces manual memoization needs
- Improves performance automatically
- Can be enabled incrementally
-
Adopt Vitest for Testing:
- Faster than Jest
- Better TypeScript support
- Modern developer experience
-
Implement Security Headers:
- Content Security Policy
- HTTPOnly cookies
- Input validation
-
Use TypeScript:
- Catch errors at compile-time
- Better IDE support
- Self-documenting code
New Project Setup #
-
Use a Modern Framework:
- Next.js 15+ for full-stack apps
- Vite for SPAs
- Remix for edge-first apps
-
Project Structure:
src/ ├── components/ # Reusable UI components ├── hooks/ # Custom hooks ├── pages/ # Route components ├── services/ # API calls, business logic ├── utils/ # Helper functions ├── types/ # TypeScript types └── test/ # Test utilities -
Essential Dependencies:
1{ 2 "dependencies": { 3 "react": "^19.0.0", 4 "react-dom": "^19.0.0", 5 "react-router-dom": "^6.20.0" 6 }, 7 "devDependencies": { 8 "@vitejs/plugin-react": "^4.2.0", 9 "typescript": "^5.3.0", 10 "vitest": "^1.0.0", 11 "@testing-library/react": "^14.1.0", 12 "@testing-library/user-event": "^14.5.0", 13 "eslint": "^8.55.0", 14 "prettier": "^3.1.0" 15 } 16} -
Configuration Files:
tsconfig.json- TypeScript configvite.config.ts- Build + test config.eslintrc.js- Linting rules.prettierrc- Code formatting
Best Practices Checklist #
Development:
- Use functional components exclusively
- Create custom hooks for reusable logic
- Leverage React 19's new hooks (use, useActionState, useOptimistic)
- Minimize useEffect usage
- Use TypeScript for type safety
- Follow component composition patterns
Performance:
- Enable React Compiler
- Use Suspense for data fetching
- Implement lazy loading for routes
- Use virtualization for long lists
- Optimize images (WebP, lazy loading)
- Profile regularly with React DevTools
Testing:
- Use Vitest + React Testing Library
- Test user behavior, not implementation
- Achieve >80% code coverage
- Test async operations properly
- Mock external dependencies
- Use Browser Mode for accuracy
Security:
- Sanitize user-generated HTML
- Validate URLs before rendering
- Use HTTPOnly cookies for auth
- Implement CSP headers
- Keep dependencies updated
- Validate all inputs (client + server)
- Use environment variables for secrets
Code Quality:
- Set up ESLint with React rules
- Use Prettier for formatting
- Implement pre-commit hooks (Husky)
- Write meaningful commit messages
- Conduct code reviews
- Document complex logic
Learning Resources #
Official Documentation:
Community Resources:
Tools:
- React DevTools - Performance profiling
- ESLint - Code linting
- Prettier - Code formatting
- TypeScript - Type checking
- Vitest - Testing framework
Conclusion #
React 19 represents a significant evolution in React development, focusing on:
- Automatic Optimization: The React Compiler eliminates most manual memoization
- Better Async Patterns: New hooks make form handling and optimistic updates trivial
- Server-First Architecture: Server Components and Actions reduce client-side complexity
- Improved DX: Simplified APIs, better error messages, and reduced boilerplate
Key Takeaways:
- Upgrade gradually: React 19 supports incremental adoption
- Embrace new patterns: Learn the new hooks and server-side features
- Prioritize security: Implement proper validation, sanitization, and headers
- Use modern tooling: Vitest, TypeScript, and React Compiler improve productivity
- Test thoroughly: Invest in comprehensive testing for reliability
- Monitor performance: Profile regularly and optimize based on data
React 19 is production-ready and recommended for all new projects. Existing projects should plan migration to take advantage of performance improvements and developer experience enhancements.
Sources #
- React v19 – React
- React 19 Upgrade Guide – React
- React 19 New Features and Migration Guide
- React 19 – New Hooks Explained with Examples
- React Design Patterns and Best Practices for 2025
- React 19 Memoization: Is useMemo & useCallback No Longer Necessary?
- React Performance Optimization: Best Practices for 2025
- React 19.2 Brings Suspense Batching to Server Rendering
- React and TypeScript Trends in 2024
- TypeScript Best Practices for 2024
- React Testing with Vitest & React Testing Library
- Vitest with React Testing Library
- Are you still using Jest for Testing your React Apps in 2025?
- Secure Code Best Practices for React 2025
- React Security Best Practices 2025
- React XSS Guide: Understanding and Prevention
- Is React Vulnerable to XSS?