react-19
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 for the mental model changes.
See 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 | <Context.Provider value={}> |
<Context value={}> |
| 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 <title>, <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:
// 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 for details.
ref as Prop (forwardRef Deprecated)
// 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
// 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
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
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
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
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)
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 for complete API details.
Form Actions
// 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
// Automatically hoisted to <head> - works in any component
function BlogPost({ post }) {
return (
<article>
<title>{post.title}</title>
<meta name="description" content={post.excerpt} />
<meta name="author" content={post.author} />
<link rel="canonical" href={post.url} />
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
Resource Preloading
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 <main>...</main>;
}
Stylesheet Support
// precedence controls insertion order and deduplication
function Component() {
return (
<>
<link rel="stylesheet" href="/base.css" precedence="default" />
<link rel="stylesheet" href="/theme.css" precedence="high" />
<div className="styled">Content</div>
</>
);
}
// React ensures stylesheets load before Suspense boundary reveals
<Suspense fallback={<Skeleton />}>
<link rel="stylesheet" href="/feature.css" precedence="default" />
<FeatureComponent />
</Suspense>
Custom Elements Support
React 19 adds full support for Custom Elements (Web Components).
// Props matching element properties are assigned as properties
// Others are assigned as attributes
<my-element
stringAttr="hello" // Attribute (string)
complexProp={{ foo: 'bar' }} // Property (object)
onCustomEvent={handleEvent} // Property (function)
/>
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.
// 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
<my-element data={{ items: [1, 2, 3] }} />
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.
<App>
<span>
+ Client
- Server
Third-Party Script Compatibility
React 19 gracefully handles elements inserted by browser extensions or third-party scripts:
- Unexpected tags in
<head>and<body>are skipped (no mismatch errors) - Stylesheets from extensions are preserved during re-renders
- No need to add
suppressHydrationWarningfor 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 for migration guides.
TypeScript Changes
// useRef requires argument
const ref = useRef<HTMLDivElement>(null); // Required
const ref = useRef(); // Error in React 19
// Ref callbacks must not return values (except cleanup)
<div ref={(node) => { instance = node; }} /> // Correct
<div ref={(node) => (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 for codemods.
Migration Codemods
# 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)
// 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
// 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 for Suspense patterns and error boundaries.
React 19.2+ Features
Activity Component (19.2)
Hide/show UI while preserving state (like background tabs):
import { Activity } from 'react';
// State preserved when hidden, Effects cleaned up
<Activity mode={isVisible ? 'visible' : 'hidden'}>
<ExpensiveComponent />
</Activity>
useEffectEvent Hook (19.2)
Extract non-reactive logic from Effects without adding dependencies:
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 for complete 19.1+ and 19.2 features.
Reference Documentation
| Document | Content |
|---|---|
| paradigm-shifts.md | Mental model changes - how to think in React 19 |
| anti-patterns.md | What to stop doing - outdated patterns |
| react-19-2-features.md | React 19.1+ and 19.2 features (Activity, useEffectEvent) |
| new-hooks.md | Complete API for 19.0 hooks |
| server-components.md | RSC, Server Actions, directives |
| suspense-streaming.md | Suspense, streaming, error handling |
| react-compiler.md | Automatic memoization details |
| deprecations.md | Removed APIs with migration guides |
| typescript-changes.md | Type changes and codemods |
More from noklip-io/agent-skills
three-js
|
71gsap
Use when implementing web animations, timeline sequencing, scroll-triggered animations, SVG animations, layout transitions, or using GSAP, ScrollTrigger, ScrollSmoother, SplitText, Flip, DrawSVG, MorphSVG, MotionPath, or @gsap/react useGSAP hook.
51theatre-js
Use when implementing motion design, timeline animations, visual animation editors, animating Three.js/R3F scenes, creating keyframe animations, or using Theatre.js, @theatre/core, @theatre/studio, @theatre/r3f, theatric, or building animation tooling for the web.
37payload
Use when working with Payload CMS projects (payload.config.ts, collections, fields, hooks, access control, Payload API). Use when debugging validation errors, security issues, relationship queries, transactions, or hook behavior.
36nuqs
Use when implementing URL query state in React, managing search params, syncing state with URL, building filterable/sortable lists, pagination with URL state, or using nuqs/useQueryState/useQueryStates hooks in Next.js, Remix, React Router, or plain React.
33base-ui
Base UI reference and workflows for @base-ui/react (unstyled, accessible React components, composition utilities, and form helpers). Use when implementing Base UI components, portals, styling/state hooks, render-prop composition, eventDetails customization, animations, forms/validation, TypeScript typing, CSP/RTL utilities, or checking Base UI docs, issues, or releases.
11