React 19 Best Practices Guide (2024-2025)

· combray's blog


Report Date: November 30, 2025 Research Focus: React 19 development best practices, new features, patterns, and security


Table of Contents #

  1. React 19 Overview
  2. New Features and Migration Patterns
  3. Component Patterns and Hooks Best Practices
  4. Performance Optimization
  5. TypeScript Integration Patterns
  6. Testing React Components
  7. Security Considerations
  8. 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 #


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:

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:

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:

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:

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:

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:

Server Actions:

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 #

  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:

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 #

  1. 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
    
  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:

 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) #

  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
 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:

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:

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:


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?

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 #


Recommendations Summary #

Immediate Actions for Existing Projects #

  1. Upgrade to React 19:

    1npx codemod@latest react/19/migration-recipe
    2npm install react@^19.0.0 react-dom@^19.0.0
    
  2. Enable React Compiler:

    • Reduces manual memoization needs
    • Improves performance automatically
    • Can be enabled incrementally
  3. Adopt Vitest for Testing:

    • Faster than Jest
    • Better TypeScript support
    • Modern developer experience
  4. Implement Security Headers:

    • Content Security Policy
    • HTTPOnly cookies
    • Input validation
  5. Use TypeScript:

    • Catch errors at compile-time
    • Better IDE support
    • Self-documenting code

New Project Setup #

  1. Use a Modern Framework:

    • Next.js 15+ for full-stack apps
    • Vite for SPAs
    • Remix for edge-first apps
  2. 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
    
  3. 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}
    
  4. Configuration Files:

    • tsconfig.json - TypeScript config
    • vite.config.ts - Build + test config
    • .eslintrc.js - Linting rules
    • .prettierrc - Code formatting

Best Practices Checklist #

Development:

Performance:

Testing:

Security:

Code Quality:

Learning Resources #

Official Documentation:

Community Resources:

Tools:


Conclusion #

React 19 represents a significant evolution in React development, focusing on:

  1. Automatic Optimization: The React Compiler eliminates most manual memoization
  2. Better Async Patterns: New hooks make form handling and optimistic updates trivial
  3. Server-First Architecture: Server Components and Actions reduce client-side complexity
  4. Improved DX: Simplified APIs, better error messages, and reduced boilerplate

Key Takeaways:

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 #

last updated: