react-19

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.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "react-19" with this command: npx skills add noklip-io/agent-skills/noklip-io-agent-skills-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:

ChangeIntroducedReact 19 Status
createRoot / hydrateRootReact 18Required (ReactDOM.render removed)
Concurrent renderingReact 18Foundation for all R19 features
Automatic batchingReact 18Default behavior
useId, useSyncExternalStoreReact 18Stable, commonly used
Hooks (no classes for new code)React 16.8Only path for new features
createContext (not legacy)React 16.3Required (legacy Context removed)
Error BoundariesReact 16Now 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 ThinkingNew Thinking
Client-side by defaultServer-first (RSC default)
Manual memoizationCompiler handles it
useEffect for dataasync Server Components
useState for formsForm Actions
Loading state booleansSuspense boundaries
Optimize everythingWrite 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

FeatureReact 18React 19+
MemoizationManual (useMemo, useCallback, memo)React Compiler (automatic) or manual
Forward refsforwardRef() wrapperref as regular prop
Context provider<Context.Provider value={}><Context value={}>
Form stateCustom with useStateuseActionState hook
Optimistic updatesManual state managementuseOptimistic hook
Read promisesNot possible in renderuse() hook
Conditional contextNot possibleuse(Context) after conditionals
Form pending stateManual trackinguseFormStatus hook
Ref cleanupPass null on unmountReturn cleanup function
Document metadatareact-helmet or manualNative <title>, <meta>, <link>
Hide/show UI with stateUnmount/remount (state lost)<Activity> component (19.2+)
Non-reactive Effect logicAdd to deps or suppress lintuseEffectEvent hook (19.2+)
Custom ElementsPartial supportFull support (props as properties)
Hydration errorsMultiple vague errorsSingle 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 suppressHydrationWarning for extension-injected content

Removed APIs (Breaking)

RemovedMigration
ReactDOM.render()createRoot().render()
ReactDOM.hydrate()hydrateRoot()
unmountComponentAtNode()root.unmount()
ReactDOM.findDOMNode()Use refs
propTypesTypeScript or remove
defaultProps (functions)ES6 default parameters
String refsCallback refs or useRef
Legacy ContextcreateContext
React.createFactoryJSX
react-dom/test-utilsact 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

DocumentContent
paradigm-shifts.mdMental model changes - how to think in React 19
anti-patterns.mdWhat to stop doing - outdated patterns
react-19-2-features.mdReact 19.1+ and 19.2 features (Activity, useEffectEvent)
new-hooks.mdComplete API for 19.0 hooks
server-components.mdRSC, Server Actions, directives
suspense-streaming.mdSuspense, streaming, error handling
react-compiler.mdAutomatic memoization details
deprecations.mdRemoved APIs with migration guides
typescript-changes.mdType changes and codemods

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

three-js

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

gsap

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

payload

No summary provided by upstream source.

Repository SourceNeeds Review