--- name: react-19 description: > React 19 patterns and breaking changes vs React 18. Trigger: When writing React 19 components/hooks in .tsx (ref as prop, new hooks, Actions, deprecations). If using Next.js App Router/Server Actions, also use nextjs-15. license: MIT metadata: author: noklip-io version: "1.0" scope: [root, ui] auto_invoke: "Writing React 19 components" allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task --- # React 19 - Key Changes This skill focuses on **what changed** in React 19. Not a complete React reference. ## Coming from React 16/17? If upgrading from pre-18 versions, these changes accumulated and are **now mandatory**: | Change | Introduced | React 19 Status | |--------|------------|-----------------| | `createRoot` / `hydrateRoot` | React 18 | **Required** (`ReactDOM.render` removed) | | Concurrent rendering | React 18 | Foundation for all R19 features | | Automatic batching | React 18 | Default behavior | | `useId`, `useSyncExternalStore` | React 18 | Stable, commonly used | | Hooks (no classes for new code) | React 16.8 | Only path for new features | | `createContext` (not legacy) | React 16.3 | **Required** (legacy Context removed) | | Error Boundaries | React 16 | Now with better error callbacks | **Migration path:** Upgrade to React 18.3 first (shows deprecation warnings), then to 19. ## The React 19 Mindset React 19 represents fundamental shifts in how to think about React: | Old Thinking | New Thinking | |--------------|--------------| | Client-side by default | **Server-first** (RSC default) | | Manual memoization | **Compiler handles it** | | `useEffect` for data | **async Server Components** | | `useState` for forms | **Form Actions** | | Loading state booleans | **Suspense boundaries** | | Optimize everything | **Write correct code, compiler optimizes** | See [references/paradigm-shifts.md](./references/paradigm-shifts.md) for the mental model changes. See [references/anti-patterns.md](./references/anti-patterns.md) for what to stop doing. ## Quick Reference: What's New | Feature | React 18 | React 19+ | |---------|----------|-----------| | Memoization | Manual (`useMemo`, `useCallback`, `memo`) | React Compiler (automatic) or manual | | Forward refs | `forwardRef()` wrapper | `ref` as regular prop | | Context provider | `` | `` | | Form state | Custom with `useState` | `useActionState` hook | | Optimistic updates | Manual state management | `useOptimistic` hook | | Read promises | Not possible in render | `use()` hook | | Conditional context | Not possible | `use(Context)` after conditionals | | Form pending state | Manual tracking | `useFormStatus` hook | | Ref cleanup | Pass `null` on unmount | Return cleanup function | | Document metadata | `react-helmet` or manual | Native ``, `<meta>`, `<link>` | | Hide/show UI with state | Unmount/remount (state lost) | `<Activity>` component (19.2+) | | Non-reactive Effect logic | Add to deps or suppress lint | `useEffectEvent` hook (19.2+) | | Custom Elements | Partial support | Full support (props as properties) | | Hydration errors | Multiple vague errors | Single error with diff | ## React Compiler & Memoization With React Compiler enabled, manual memoization is **optional, not forbidden**: ```tsx // React Compiler handles this automatically function Component({ items }) { const filtered = items.filter(x => x.active); const sorted = filtered.sort((a, b) => a.name.localeCompare(b.name)); const handleClick = (id) => console.log(id); return <List items={sorted} onClick={handleClick} />; } // Manual memoization still works as escape hatch for fine-grained control const filtered = useMemo(() => expensiveOperation(items), [items]); const handleClick = useCallback((id) => onClick(id), [onClick]); ``` **When to use manual memoization with React Compiler:** - Effect dependencies that need stable references - Sharing expensive calculations across components (compiler doesn't share) - Explicit control over when re-computation happens See [references/react-compiler.md](./references/react-compiler.md) for details. ## ref as Prop (forwardRef Deprecated) ```tsx // React 19: ref is just a prop function Input({ placeholder, ref }) { return <input placeholder={placeholder} ref={ref} />; } // Usage - no change const inputRef = useRef(null); <Input ref={inputRef} placeholder="Enter text" /> // forwardRef still works but will be deprecated // Codemod: npx codemod@latest react/19/replace-forward-ref ``` ## Ref Cleanup Functions ```tsx // React 19: Return cleanup function from ref callback <input ref={(node) => { // Setup node?.focus(); // Return cleanup (called on unmount or ref change) return () => { console.log('Cleanup'); }; }} /> // React 18: Received null on unmount (still works, but cleanup preferred) <input ref={(node) => { if (node) { /* setup */ } else { /* cleanup */ } }} /> ``` ## Context as Provider ```tsx const ThemeContext = createContext('light'); // React 19: Use Context directly function App({ children }) { return ( <ThemeContext value="dark"> {children} </ThemeContext> ); } // React 18: Required .Provider (still works, will be deprecated) <ThemeContext.Provider value="dark"> {children} </ThemeContext.Provider> ``` ## New Hooks ### useActionState ```tsx import { useActionState } from 'react'; function Form() { const [error, submitAction, isPending] = useActionState( async (prevState, formData) => { const result = await saveData(formData.get('name')); if (result.error) return result.error; redirect('/success'); return null; }, null // initial state ); return ( <form action={submitAction}> <input name="name" disabled={isPending} /> <button disabled={isPending}> {isPending ? 'Saving...' : 'Save'} </button> {error && <p className="error">{error}</p>} </form> ); } ``` ### useOptimistic ```tsx import { useOptimistic } from 'react'; function Messages({ messages, sendMessage }) { const [optimisticMessages, addOptimistic] = useOptimistic( messages, (state, newMessage) => [...state, { ...newMessage, sending: true }] ); async function handleSubmit(formData) { const message = { text: formData.get('text'), id: Date.now() }; addOptimistic(message); // Show immediately await sendMessage(message); // Reverts on error } return ( <form action={handleSubmit}> {optimisticMessages.map(m => ( <div key={m.id} style={{ opacity: m.sending ? 0.5 : 1 }}> {m.text} </div> ))} <input name="text" /> </form> ); } ``` ### use() Hook ```tsx import { use, Suspense } from 'react'; // Read promises (suspends until resolved) function Comments({ commentsPromise }) { const comments = use(commentsPromise); return comments.map(c => <p key={c.id}>{c.text}</p>); } // Usage with Suspense <Suspense fallback={<Spinner />}> <Comments commentsPromise={fetchComments()} /> </Suspense> // Conditional context reading (not possible with useContext!) function Theme({ showTheme }) { if (!showTheme) return <div>Plain</div>; const theme = use(ThemeContext); // Can be called conditionally! return <div style={{ color: theme.primary }}>Themed</div>; } ``` ### useFormStatus (react-dom) ```tsx import { useFormStatus } from 'react-dom'; // Must be used inside a <form> - reads parent form status function SubmitButton() { const { pending, data, method, action } = useFormStatus(); return ( <button disabled={pending}> {pending ? 'Submitting...' : 'Submit'} </button> ); } function Form() { return ( <form action={serverAction}> <input name="email" /> <SubmitButton /> {/* Reads form status via context */} </form> ); } ``` See [references/new-hooks.md](./references/new-hooks.md) for complete API details. ## Form Actions ```tsx // Pass function directly to form action <form action={async (formData) => { 'use server'; await saveToDatabase(formData); }}> <input name="email" type="email" /> <button type="submit">Subscribe</button> </form> // Button-level actions <form> <button formAction={saveAction}>Save</button> <button formAction={deleteAction}>Delete</button> </form> // Manual form reset import { requestFormReset } from 'react-dom'; requestFormReset(formElement); ``` ## Document Metadata ```tsx // Automatically hoisted to <head> - works in any component function BlogPost({ post }) { return ( <article> <title>{post.title}

{post.title}

{post.content}

); } ``` ## Resource Preloading ```tsx import { prefetchDNS, preconnect, preload, preinit } from 'react-dom'; function App() { // DNS prefetch prefetchDNS('https://api.example.com'); // Establish connection early preconnect('https://fonts.googleapis.com'); // Preload resources preload('https://example.com/font.woff2', { as: 'font' }); preload('/hero.jpg', { as: 'image' }); // Load and execute script eagerly preinit('https://example.com/analytics.js', { as: 'script' }); return
...
; } ``` ## Stylesheet Support ```tsx // precedence controls insertion order and deduplication function Component() { return ( <>
Content
); } // React ensures stylesheets load before Suspense boundary reveals }> ``` ## Custom Elements Support React 19 adds full support for Custom Elements (Web Components). ```tsx // Props matching element properties are assigned as properties // Others are assigned as attributes ``` **Client-side:** React checks if a property exists on the element instance. If yes, assigns as property; otherwise, as attribute. **Server-side (SSR):** Primitive types (string, number) render as attributes. Objects, functions, symbols are omitted from HTML. ```tsx // Define custom element class MyElement extends HTMLElement { constructor() { super(); this.data = undefined; // React will assign to this property } connectedCallback() { this.textContent = JSON.stringify(this.data); } } customElements.define('my-element', MyElement); // Use in React ``` ## Hydration Improvements ### Better Error Messages React 19 shows a single error with a diff instead of multiple vague errors: ``` Uncaught Error: Hydration failed because the server rendered HTML didn't match the client. + Client - Server ``` ### Third-Party Script Compatibility React 19 gracefully handles elements inserted by browser extensions or third-party scripts: - Unexpected tags in `` and `` are skipped (no mismatch errors) - Stylesheets from extensions are preserved during re-renders - No need to add `suppressHydrationWarning` for extension-injected content ## Removed APIs (Breaking) | Removed | Migration | |---------|-----------| | `ReactDOM.render()` | `createRoot().render()` | | `ReactDOM.hydrate()` | `hydrateRoot()` | | `unmountComponentAtNode()` | `root.unmount()` | | `ReactDOM.findDOMNode()` | Use refs | | `propTypes` | TypeScript or remove | | `defaultProps` (functions) | ES6 default parameters | | String refs | Callback refs or `useRef` | | Legacy Context | `createContext` | | `React.createFactory` | JSX | | `react-dom/test-utils` | `act` from `'react'` | See [references/deprecations.md](./references/deprecations.md) for migration guides. ## TypeScript Changes ```tsx // useRef requires argument const ref = useRef(null); // Required const ref = useRef(); // Error in React 19 // Ref callbacks must not return values (except cleanup)
{ instance = node; }} /> // Correct
(instance = node)} /> // Error - implicit return // ReactElement props are now unknown (not any) type Props = ReactElement['props']; // unknown in R19, any in R18 // JSX namespace - import explicitly import type { JSX } from 'react'; ``` See [references/typescript-changes.md](./references/typescript-changes.md) for codemods. ## Migration Codemods ```bash # Run all React 19 codemods npx codemod@latest react/19/migration-recipe # Individual codemods npx codemod@latest react/19/replace-reactdom-render npx codemod@latest react/19/replace-string-ref npx codemod@latest react/19/replace-act-import npx codemod@latest react/19/replace-use-form-state npx codemod@latest react/prop-types-typescript # TypeScript types npx types-react-codemod@latest preset-19 ./src ``` ## Imports (Best Practice) ```tsx // Named imports (recommended) import { useState, useEffect, useRef, use } from 'react'; import { createRoot } from 'react-dom/client'; import { useFormStatus } from 'react-dom'; // Default import still works but named preferred import React from 'react'; // Works but not recommended ``` ## Error Handling Changes ```tsx // React 19 error handling options const root = createRoot(container, { onUncaughtError: (error, errorInfo) => { // Errors not caught by Error Boundary console.error('Uncaught:', error, errorInfo.componentStack); }, onCaughtError: (error, errorInfo) => { // Errors caught by Error Boundary reportToAnalytics(error); }, onRecoverableError: (error, errorInfo) => { // Errors React recovered from automatically console.warn('Recovered:', error); } }); ``` See [references/suspense-streaming.md](./references/suspense-streaming.md) for Suspense patterns and error boundaries. ## React 19.2+ Features ### Activity Component (19.2) Hide/show UI while preserving state (like background tabs): ```tsx import { Activity } from 'react'; // State preserved when hidden, Effects cleaned up ``` ### useEffectEvent Hook (19.2) Extract non-reactive logic from Effects without adding dependencies: ```tsx import { useEffect, useEffectEvent } from 'react'; function Chat({ roomId, theme }) { // Reads theme without making it a dependency const onConnected = useEffectEvent(() => { showNotification(`Connected!`, theme); }); useEffect(() => { const conn = connect(roomId); conn.on('connected', onConnected); return () => conn.disconnect(); }, [roomId]); // theme NOT in deps - won't reconnect on theme change } ``` See [references/react-19-2-features.md](./references/react-19-2-features.md) for complete 19.1+ and 19.2 features. ## Reference Documentation | Document | Content | |----------|---------| | [paradigm-shifts.md](./references/paradigm-shifts.md) | Mental model changes - how to *think* in React 19 | | [anti-patterns.md](./references/anti-patterns.md) | What to stop doing - outdated patterns | | [react-19-2-features.md](./references/react-19-2-features.md) | React 19.1+ and 19.2 features (Activity, useEffectEvent) | | [new-hooks.md](./references/new-hooks.md) | Complete API for 19.0 hooks | | [server-components.md](./references/server-components.md) | RSC, Server Actions, directives | | [suspense-streaming.md](./references/suspense-streaming.md) | Suspense, streaming, error handling | | [react-compiler.md](./references/react-compiler.md) | Automatic memoization details | | [deprecations.md](./references/deprecations.md) | Removed APIs with migration guides | | [typescript-changes.md](./references/typescript-changes.md) | Type changes and codemods |