React Upgrade Guide

Compare React versions and see exactly what changes, what requires action, and what you can start using.

60Changes
19Breaking
30Actions
21Features
0Migration tools
Analyze your project

Paste your package.json

UpgradePath detects relevant tools and selects them automatically.

Local only
Analyzed locally in your browser. Nothing is uploaded.
Project setup

What does your project use?

Select the tools in your project to include relevant dependency updates in this guide.

Breaking Changes

ReactDOM.render should be replaced with createRoot

Action required
breakinghigh impact

React 18 introduces the new root API. Applications that continue using ReactDOM.render run in React 17 compatibility mode and cannot use the new React 18 concurrent features.

Beforetsx
import ReactDOM from "react-dom";

const container = document.getElementById("root");

ReactDOM.render(
  <App />,
  container,
);
Aftertsx
import { createRoot } from "react-dom/client";

const container =
  document.getElementById("root");

const root = createRoot(container!);

root.render(<App />);
Migration

Import createRoot from react-dom/client, create the root once, and call root.render().

ReactDOM.hydrate should be replaced with hydrateRoot

Action required
breakinghigh impact

Server-rendered React applications should migrate from the legacy hydrate API to hydrateRoot from react-dom/client.

Beforetsx
import { hydrate } from "react-dom";

const container =
  document.getElementById("root");

hydrate(
  <App />,
  container,
);
Aftertsx
import { hydrateRoot } from "react-dom/client";

const container =
  document.getElementById("root");

hydrateRoot(
  container!,
  <App />,
);
Migration

Replace hydrate with hydrateRoot. Unlike createRoot, hydrateRoot receives the initial React tree directly.

The legacy render callback is no longer supported

Action required
breakingmedium impact

The new root.render API does not support the callback argument previously accepted by ReactDOM.render.

Beforetsx
ReactDOM.render(
  <App />,
  container,
  () => {
    console.log("rendered");
  },
);
Aftertsx
function AppWithCallback() {
  useEffect(() => {
    console.log("rendered");
  }, []);

  return <App />;
}

root.render(
  <AppWithCallback />,
);
Migration

Move callback behavior to an appropriate application mechanism such as an effect. React notes that there is no universal one-to-one replacement for the old render callback.

Hydration mismatches are treated more strictly

Action required
breakinghigh impact

Missing or extra text content during hydration is treated as an error rather than a warning. React may fall back to client rendering up to the closest Suspense boundary instead of attempting to patch individual mismatched nodes.

Migration

Fix server/client markup mismatches instead of relying on React to patch them during hydration.

The legacy render callback is not supported by createRoot

Action required
breakingmedium impact

The new root API does not support the third callback argument previously accepted by ReactDOM.render.

Beforetsx
ReactDOM.render(
  <App />,
  container,
  () => {
    console.log("rendered");
  },
);
Aftertsx
function AppWithCallback() {
  useEffect(() => {
    console.log("rendered");
  }, []);

  return <App />;
}

const root =
  createRoot(container);

root.render(
  <AppWithCallback />,
);
Migration

Move post-render work to an appropriate mechanism such as an effect, a ref callback, or another API suited to the specific use case. React does not provide a one-to-one replacement for the old render callback.

Replace unmountComponentAtNode with root.unmount

Action required
breakingmedium impact

Applications adopting the React 18 root API should unmount through the root object instead of calling unmountComponentAtNode.

Beforetsx
import {
  unmountComponentAtNode,
} from "react-dom";

unmountComponentAtNode(
  container,
);
Aftertsx
const root =
  createRoot(container);

root.render(<App />);

// Later:
root.unmount();
Migration

Keep the root returned by createRoot and call root.unmount() when the application needs to be removed.

The modern JSX transform is required

Action required
breakinghigh impact

React 19 requires the modern JSX transform. Projects still using the legacy transform should update their build configuration before upgrading.

Migration

Ensure your build tool uses the modern JSX transform before upgrading to React 19.

propTypes are removed for function components

Action required
breakingmedium impact

React 19 removes runtime propTypes checks for function components. Existing propTypes declarations on function components are ignored.

Beforejsx
import PropTypes from "prop-types";

function Heading({ text }) {
  return <h1>{text}</h1>;
}

Heading.propTypes = {
  text: PropTypes.string,
};
Aftertsx
interface HeadingProps {
  text?: string;
}

function Heading({
  text,
}: HeadingProps) {
  return <h1>{text}</h1>;
}
Migration

Remove function-component propTypes and migrate to TypeScript or another type-checking solution.

defaultProps are removed for function components

Action required
breakingmedium impact

React 19 removes defaultProps support from function components. Class components continue to support defaultProps.

Beforetsx
function Heading({
  text,
}: {
  text?: string;
}) {
  return <h1>{text}</h1>;
}

Heading.defaultProps = {
  text: "Hello",
};
Aftertsx
function Heading({
  text = "Hello",
}: {
  text?: string;
}) {
  return <h1>{text}</h1>;
}
Migration

Replace function-component defaultProps with JavaScript default parameters.

Legacy Context APIs are removed

Action required
breakinghigh impact

React 19 removes the legacy contextTypes and getChildContext APIs that were deprecated in React 16.6.

Beforejsx
class Parent extends React.Component {
  getChildContext() {
    return {
      theme: "dark",
    };
  }

  render() {
    return <Child />;
  }
}

Parent.childContextTypes = {
  theme: PropTypes.string,
};
Aftertsx
const ThemeContext =
  React.createContext("light");

function Parent() {
  return (
    <ThemeContext value="dark">
      <Child />
    </ThemeContext>
  );
}
Migration

Replace contextTypes and getChildContext with createContext and the modern Context API.

String refs are removed

Action required
breakinghigh impact

React 19 removes string refs from class components. Ref callbacks or createRef should be used instead.

Beforejsx
class Form extends React.Component {
  componentDidMount() {
    this.refs.input.focus();
  }

  render() {
    return (
      <input ref="input" />
    );
  }
}
Afterjsx
class Form extends React.Component {
  input = null;

  componentDidMount() {
    this.input?.focus();
  }

  render() {
    return (
      <input
        ref={(input) => {
          this.input = input;
        }}
      />
    );
  }
}
Migration

Replace string refs with callback refs or createRef. React provides a codemod for this migration.

Module pattern factories are removed

Action required
breakinglow impact

React 19 removes support for the legacy module pattern factory component style.

Beforejsx
function FactoryComponent() {
  return {
    render() {
      return <div>Hello</div>;
    },
  };
}
Afterjsx
function FactoryComponent() {
  return <div>Hello</div>;
}
Migration

Convert module pattern factories into normal function components.

React.createFactory is removed

Action required
breakinglow impact

React 19 removes React.createFactory, an API that predates widespread JSX adoption.

Beforejavascript
import {
  createFactory,
} from "react";

const Button =
  createFactory("button");

const element =
  Button({
    children: "Save",
  });
Afterjsx
const element = (
  <button>
    Save
  </button>
);
Migration

Replace React.createFactory usage with JSX.

react-test-renderer/shallow is removed

Action required
breakingmedium impact

React 19 removes the react-test-renderer/shallow entry point. React recommends moving away from shallow rendering.

Beforetypescript
import ShallowRenderer from
  "react-test-renderer/shallow";
Aftertypescript
import ShallowRenderer from
  "react-shallow-renderer";
Migration

Prefer migrating tests to React Testing Library. If shallow rendering must temporarily remain, install react-shallow-renderer directly.

Import act from react instead of react-dom/test-utils

Action required
breakingmedium impact

React 19 moves act to the React package. The react-dom/test-utils version is deprecated and the other legacy test utilities are removed.

Beforetypescript
import {
  act,
} from "react-dom/test-utils";
Aftertypescript
import {
  act,
} from "react";
Migration

Import act directly from react. Review other react-dom/test-utils usage and migrate away from low-level test utilities.

ReactDOM.render is removed

Action required
breakinghigh impact

React 19 removes ReactDOM.render after its deprecation in React 18. Applications must use createRoot.

Beforetsx
import {
  render,
} from "react-dom";

render(
  <App />,
  document.getElementById("root"),
);
Aftertsx
import {
  createRoot,
} from "react-dom/client";

const container =
  document.getElementById("root");

const root =
  createRoot(container!);

root.render(<App />);
Migration

Replace ReactDOM.render with createRoot from react-dom/client. The React 19 migration recipe includes a codemod for this change.

ReactDOM.hydrate is removed

Action required
breakinghigh impact

React 19 removes the legacy ReactDOM.hydrate API. Server-rendered applications must use hydrateRoot.

Beforetsx
import {
  hydrate,
} from "react-dom";

hydrate(
  <App />,
  document.getElementById("root"),
);
Aftertsx
import {
  hydrateRoot,
} from "react-dom/client";

hydrateRoot(
  document.getElementById("root")!,
  <App />,
);
Migration

Replace ReactDOM.hydrate with hydrateRoot from react-dom/client.

unmountComponentAtNode is removed

Action required
breakingmedium impact

React 19 removes ReactDOM.unmountComponentAtNode. Applications using the root API should unmount through the root instance.

Beforetypescript
import {
  unmountComponentAtNode,
} from "react-dom";

unmountComponentAtNode(
  container,
);
Aftertypescript
const root =
  createRoot(container);

root.render(<App />);

// Later:
root.unmount();
Migration

Keep the root returned by createRoot and call root.unmount() when the application needs to be removed.

ReactDOM.findDOMNode is removed

Action required
breakinghigh impact

React 19 removes findDOMNode. The API was a legacy escape hatch that broke component abstraction and was fragile during refactoring.

Beforetsx
import {
  findDOMNode,
} from "react-dom";

useEffect(() => {
  const input =
    findDOMNode(component);

  input?.focus();
}, []);
Aftertsx
const inputRef =
  useRef<HTMLInputElement>(null);

useEffect(() => {
  inputRef.current?.focus();
}, []);

return (
  <input ref={inputRef} />
);
Migration

Replace findDOMNode with an explicit DOM ref.

New Features

flushSync can opt out of automatic batching

featuremedium impact

React 18 provides flushSync for rare cases where application code must force React to synchronously flush an update before continuing.

Aftertsx
import { flushSync } from "react-dom";

flushSync(() => {
  setCount((count) => count + 1);
});

// DOM has been updated.

flushSync(() => {
  setFlag((flag) => !flag);
});
Migration

Prefer React 18 automatic batching. Use flushSync only when code truly requires synchronous DOM updates.

renderToReadableStream supports modern edge runtimes

featuremedium impact

React 18 introduces renderToReadableStream for streaming server rendering in environments that support Web Streams, including modern edge runtimes.

Aftertsx
import {
  renderToReadableStream,
} from "react-dom/server";

const stream =
  await renderToReadableStream(
    <App />,
  );

Transitions separate urgent and non-urgent updates

featuremedium impact

React 18 introduces startTransition and useTransition for marking state updates as non-urgent so urgent interactions such as typing can remain responsive.

Aftertsx
import {
  startTransition,
} from "react";

setInputValue(nextValue);

startTransition(() => {
  setSearchQuery(nextValue);
});

useTransition exposes transition pending state

featuremedium impact

useTransition allows components to start non-urgent updates while also exposing whether the transition is pending.

Aftertsx
import {
  useTransition,
} from "react";

const [
  isPending,
  startTransition,
] = useTransition();

function selectTab(tab: string) {
  startTransition(() => {
    setTab(tab);
  });
}

Defer non-urgent rendering with useDeferredValue

featuremedium impact

React 18 introduces useDeferredValue for deferring updates to non-urgent parts of the UI. Unlike a fixed debounce delay, deferred rendering is interruptible and adapts to rendering work.

Beforetsx
function SearchResults({
  query,
}: {
  query: string;
}) {
  return (
    <ExpensiveResults query={query} />
  );
}
Aftertsx
import {
  useDeferredValue,
} from "react";

function SearchResults({
  query,
}: {
  query: string;
}) {
  const deferredQuery =
    useDeferredValue(query);

  return (
    <ExpensiveResults
      query={deferredQuery}
    />
  );
}

Generate hydration-safe IDs with useId

featurelow impact

React 18 introduces useId for generating stable unique IDs that work across client and server rendering without causing hydration mismatches.

Beforetsx
function PasswordField() {
  const id = "password-field";

  return (
    <>
      <label htmlFor={id}>
        Password
      </label>

      <input
        id={id}
        type="password"
      />
    </>
  );
}
Aftertsx
import { useId } from "react";

function PasswordField() {
  const id = useId();

  return (
    <>
      <label htmlFor={id}>
        Password
      </label>

      <input
        id={id}
        type="password"
      />
    </>
  );
}
Official sources

External stores can use useSyncExternalStore

featuremedium impact

React 18 introduces useSyncExternalStore so libraries that integrate external stores can support concurrent rendering with synchronous external-store updates.

Beforetsx
useEffect(() => {
  return store.subscribe(() => {
    setState(store.getSnapshot());
  });
}, []);
Aftertsx
import {
  useSyncExternalStore,
} from "react";

const state = useSyncExternalStore(
  store.subscribe,
  store.getSnapshot,
  store.getServerSnapshot,
);

CSS-in-JS libraries can use useInsertionEffect

featurelow impact

React 18 introduces useInsertionEffect for CSS-in-JS library authors that need to inject styles before layout effects read the updated layout.

Migration

Application code generally does not need this Hook. It is primarily intended for CSS-in-JS library maintainers.

New streaming server rendering APIs

featurehigh impact

React 18 adds renderToPipeableStream for Node.js and renderToReadableStream for modern edge runtimes, with streaming Suspense support.

Beforetsx
import {
  renderToString,
} from "react-dom/server";

const html =
  renderToString(<App />);
Aftertsx
import {
  renderToPipeableStream,
} from "react-dom/server";

const {
  pipe,
} = renderToPipeableStream(
  <App />,
  {
    onShellReady() {
      pipe(response);
    },
  },
);

Function components can receive ref as a prop

featuremedium impact

React 19 allows function components to access ref directly as a prop, reducing the need for forwardRef in new function components.

Beforetsx
import {
  forwardRef,
} from "react";

const Input = forwardRef<
  HTMLInputElement,
  InputProps
>((props, ref) => {
  return (
    <input
      {...props}
      ref={ref}
    />
  );
});
Aftertsx
function Input({
  ref,
  ...props
}: InputProps & {
  ref?: React.Ref<HTMLInputElement>;
}) {
  return (
    <input
      {...props}
      ref={ref}
    />
  );
}
Migration

New function components can accept ref directly as a prop. Existing forwardRef components can continue to work, and React provides a codemod for migration.

Ref callbacks can return cleanup functions

featuremedium impact

React 19 supports returning a cleanup function from a ref callback. React calls the cleanup when the element is removed.

Beforetsx
<div
  ref={(node) => {
    if (node) {
      setup(node);
    }
  }}
/>
Aftertsx
<div
  ref={(node) => {
    setup(node);

    return () => {
      cleanup(node);
    };
  }}
/>

Context can be rendered directly as a provider

featurelow impact

React 19 allows <Context> to be used directly as a provider instead of requiring <Context.Provider>.

Beforetsx
<ThemeContext.Provider value="dark">
  <App />
</ThemeContext.Provider>
Aftertsx
<ThemeContext value="dark">
  <App />
</ThemeContext>

Actions manage async mutations and pending state

featurehigh impact

React 19 introduces Actions for handling asynchronous transitions, including pending states, optimistic updates, errors, and form mutations.

Beforetsx
async function submit() {
  setPending(true);

  try {
    await updateName(name);
  } finally {
    setPending(false);
  }
}
Aftertsx
const [
  isPending,
  startTransition,
] = useTransition();

function submit() {
  startTransition(async () => {
    await updateName(name);
  });
}
Official sources

useActionState manages Action results and pending state

featuremedium impact

React 19 introduces useActionState for updating state based on the result of an Action while exposing pending state.

Aftertsx
import {
  useActionState,
} from "react";

const [
  error,
  submitAction,
  isPending,
] = useActionState(
  async (
    previousState,
    formData,
  ) => {
    const error =
      await updateName(
        formData.get("name"),
      );

    return error;
  },
  null,
);

useOptimistic supports optimistic UI updates

featuremedium impact

React 19 introduces useOptimistic for temporarily showing an expected result while an asynchronous Action is in progress.

Aftertsx
import {
  useOptimistic,
} from "react";

const [
  optimisticName,
  setOptimisticName,
] = useOptimistic(name);

async function submit(
  formData: FormData,
) {
  const nextName =
    String(formData.get("name"));

  setOptimisticName(nextName);

  await updateName(nextName);
}

The use API reads resources during render

featurehigh impact

React 19 introduces use for reading resources such as promises and context during rendering. Unlike Hooks, use can be called conditionally.

Aftertsx
import {
  use,
} from "react";

function Comments({
  commentsPromise,
}: {
  commentsPromise: Promise<Comment[]>;
}) {
  const comments =
    use(commentsPromise);

  return comments.map(
    (comment) => (
      <Comment
        key={comment.id}
        comment={comment}
      />
    ),
  );
}

Forms can invoke Actions directly

featurehigh impact

React 19 extends form action, input formAction, and button formAction props so they can receive functions that handle form submissions.

Beforetsx
<form
  onSubmit={handleSubmit}
>
  <input name="name" />

  <button type="submit">
    Save
  </button>
</form>
Aftertsx
async function saveName(
  formData: FormData,
) {
  await updateName(
    formData.get("name"),
  );
}

<form action={saveName}>
  <input name="name" />

  <button type="submit">
    Save
  </button>
</form>
Official sources

useFormStatus exposes parent form status

featuremedium impact

React 19 adds useFormStatus in react-dom so components can read information about the parent form submission, including whether it is pending.

Aftertsx
import {
  useFormStatus,
} from "react-dom";

function SubmitButton() {
  const {
    pending,
  } = useFormStatus();

  return (
    <button
      disabled={pending}
      type="submit"
    >
      {pending
        ? "Saving..."
        : "Save"}
    </button>
  );
}

Metadata tags can be rendered from components

featuremedium impact

React 19 adds native support for title, meta, and link metadata tags rendered from components and hoists them to the document head.

Aftertsx
function ProductPage({
  product,
}: {
  product: Product;
}) {
  return (
    <>
      <title>
        {product.name}
      </title>

      <meta
        name="description"
        content={
          product.description
        }
      />

      <ProductDetails
        product={product}
      />
    </>
  );
}

Stylesheets support precedence and rendering coordination

featuremedium impact

React 19 adds built-in support for stylesheet links with precedence, allowing React to coordinate stylesheet loading with rendering.

Aftertsx
<link
  rel="stylesheet"
  href="/styles.css"
  precedence="default"
/>

Async scripts can be rendered from components

featurelow impact

React 19 improves support for async scripts rendered anywhere in the component tree by deduplicating and managing them as document resources.

Aftertsx
<script
  async
  src="https://example.com/sdk.js"
/>

TypeScript

TypeScript props must declare children explicitly

Action required
typescripthigh impact

React 18's updated TypeScript definitions no longer implicitly add children to many component prop types. Components that accept children should declare the prop explicitly.

Beforetsx
interface ButtonProps {
  color: string;
}

function Button(
  props: ButtonProps,
) {
  return (
    <button>
      {props.children}
    </button>
  );
}
Aftertsx
interface ButtonProps {
  color: string;
  children?: React.ReactNode;
}

function Button(
  props: ButtonProps,
) {
  return (
    <button>
      {props.children}
    </button>
  );
}
Migration

Update React TypeScript definitions and explicitly declare children where components accept them.

Implicit ref callback returns can cause TypeScript errors

Action required
typescriptmedium impact

Because React 19 ref callbacks may return cleanup functions, TypeScript no longer accepts some implicit returns that return the assigned value instead of void.

Beforetsx
<div
  ref={(current) =>
    (instance = current)
  }
/>
Aftertsx
<div
  ref={(current) => {
    instance = current;
  }}
/>
Migration

Change ref callback assignments that implicitly return a value into block-bodied callbacks that return nothing.

Performance

Resource preloading APIs are available

performancemedium impact

React 19 adds APIs for prefetching DNS, preconnecting to origins, preloading resources, and preinitializing scripts and stylesheets.

Aftertsx
import {
  preconnect,
  preload,
} from "react-dom";

preconnect(
  "https://cdn.example.com",
);

preload(
  "/fonts/inter.woff2",
  {
    as: "font",
  },
);

Tooling

Hydration errors provide improved diagnostics

toolinglow impact

React 19 improves hydration mismatch reporting by combining related messages and providing more useful information about the mismatch.

Behavior Changes

State updates are automatically batched more broadly

Action required
behaviorhigh impact

React 18 automatically batches updates from promises, timeouts, native event handlers, and other asynchronous sources when using the new root API. Code that depends on intermediate synchronous DOM updates may behave differently.

Beforetsx
setTimeout(() => {
  setCount((count) => count + 1);
  setFlag((flag) => !flag);

  // React 17 may render twice.
}, 1000);
Aftertsx
setTimeout(() => {
  setCount((count) => count + 1);
  setFlag((flag) => !flag);

  // React 18 batches these updates.
  // One render occurs.
}, 1000);
Migration

Review code that depends on observing the DOM between state updates. If synchronous DOM flushing is genuinely required, React provides flushSync as an escape hatch.

StrictMode re-runs effects in development

Action required
behaviorhigh impact

React 18 adds a development-only StrictMode check that simulates unmounting and remounting newly mounted components while restoring their previous state. Effects must tolerate repeated setup and cleanup.

Beforetsx
useEffect(() => {
  const connection =
    createConnection();

  connection.connect();

  // Missing cleanup.
}, []);
Aftertsx
useEffect(() => {
  const connection =
    createConnection();

  connection.connect();

  return () => {
    connection.disconnect();
  };
}, []);
Migration

Audit effects for correct cleanup and ensure they remain correct when setup and cleanup run multiple times during development.

Effects triggered by discrete user input have consistent timing

behaviormedium impact

React 18 consistently synchronously flushes effect functions when an update is triggered by discrete user input such as click or keydown events.

Migration

Review code or tests that depend on the previous less predictable effect timing around discrete user interactions.

Suspense no longer commits incomplete trees

behaviormedium impact

If a component suspends before a new tree has been fully added, React 18 discards the incomplete tree and retries rendering after the asynchronous work resolves.

Migration

Review components and libraries that relied on effects or DOM from partially committed Suspense trees.

Suspense cleans up and recreates layout effects

behaviormedium impact

When a Suspense tree re-suspends and returns to a fallback, React 18 cleans up layout effects and recreates them when the content becomes visible again.

Migration

Ensure layout effects correctly clean up subscriptions, observers, or measurements when Suspense hides and later restores content.

renderToString has limited Suspense support

behaviormedium impact

renderToString continues to work in React 18 but does not provide the full streaming Suspense capabilities of the new server rendering APIs.

Migration

Applications that need streaming Suspense should migrate to renderToPipeableStream or renderToReadableStream depending on the runtime.

Strict Mode re-runs effects in development

Action required
behaviorhigh impact

React 18 adds a development-only Strict Mode check that simulates unmounting and remounting components when they mount for the first time. Effects must tolerate setup and cleanup running more than once.

Beforetsx
useEffect(() => {
  const socket = connect();

  socket.subscribe();
}, []);
Aftertsx
useEffect(() => {
  const socket = connect();

  socket.subscribe();

  return () => {
    socket.unsubscribe();
    socket.disconnect();
  };
}, []);
Migration

Review effects for missing cleanup, non-idempotent setup, duplicated subscriptions, or assumptions that an effect runs only once. The additional behavior is development-only and is intended to surface concurrency-related bugs.

Suspense remounts layout effects when content reappears

Action required
behaviormedium impact

React 18 cleans up layout effects when Suspense hides content and recreates them when the content becomes visible again.

Migration

Ensure layout effects correctly clean up subscriptions and DOM-related state and can safely run again when Suspense content is restored.

Components may render undefined

behaviorlow impact

React 18 allows components to render undefined. This aligns undefined more closely with other empty render values such as null.

Beforetsx
function OptionalContent({
  visible,
}: {
  visible: boolean;
}) {
  if (!visible) {
    return null;
  }

  return <Content />;
}
Aftertsx
function OptionalContent({
  visible,
}: {
  visible: boolean;
}) {
  if (!visible) {
    return;
  }

  return <Content />;
}

Effects from discrete events flush synchronously

behaviormedium impact

React 18 synchronously flushes effects caused by discrete user events such as clicks. Code should not depend on the older timing behavior of these effects.

Suspense fallback undefined behaves like null

behaviorlow impact

In React 18, Suspense with fallback={undefined} behaves the same as fallback={null} instead of being ignored.

Beforetsx
<Suspense fallback={undefined}>
  <LazyPage />
</Suspense>
Aftertsx
<Suspense fallback={null}>
  <LazyPage />
</Suspense>

Automatic batching extends beyond React event handlers

Action required
behaviormedium impact

With a React 18 root, updates inside promises, timeouts, native event handlers, and other async contexts are batched automatically. This can change when intermediate renders occur.

Beforetsx
fetchData().then(() => {
  setCount((count) => count + 1);
  setFlag((flag) => !flag);

  // React 17:
  // two separate renders
});
Aftertsx
fetchData().then(() => {
  setCount((count) => count + 1);
  setFlag((flag) => !flag);

  // React 18 with createRoot:
  // one batched render
});
Migration

Review code that intentionally depends on separate renders between state updates. If synchronous DOM visibility is required in a rare case, ReactDOM.flushSync can opt out of batching.

Render errors are no longer re-thrown

Action required
behaviormedium impact

React 19 changes render error handling. Uncaught errors are reported to window.reportError and errors caught by Error Boundaries are reported to console.error instead of being caught and re-thrown.

Beforetsx
const root = createRoot(container);

root.render(<App />);
Aftertsx
const root = createRoot(container, {
  onUncaughtError(error, errorInfo) {
    reportError(error, errorInfo);
  },

  onCaughtError(error, errorInfo) {
    reportCaughtError(
      error,
      errorInfo,
    );
  },
});

root.render(<App />);
Migration

If your error reporting relies on React re-throwing render errors, migrate to the onUncaughtError and onCaughtError options available on createRoot and hydrateRoot.

Deprecations

unmountComponentAtNode should be replaced with root.unmount

Action required
deprecationmedium impact

The new root API owns the lifecycle of the mounted React tree. Legacy unmountComponentAtNode is deprecated in React 18.

Beforetsx
import {
  unmountComponentAtNode,
} from "react-dom";

unmountComponentAtNode(container);
Aftertsx
const root = createRoot(container);

root.render(<App />);

// Later
root.unmount();
Migration

Keep the root returned by createRoot and call root.unmount() when the application needs to be removed.

renderToNodeStream is deprecated

Action required
deprecationhigh impact

The legacy Node streaming SSR API does not support React 18 incremental Suspense streaming and is deprecated.

Beforetsx
import {
  renderToNodeStream,
} from "react-dom/server";

const stream =
  renderToNodeStream(<App />);
Aftertsx
import {
  renderToPipeableStream,
} from "react-dom/server";

const {
  pipe,
} = renderToPipeableStream(
  <App />,
  {
    onShellReady() {
      pipe(response);
    },
  },
);
Migration

For Node.js streaming SSR, migrate to renderToPipeableStream.

element.ref is deprecated

Action required
deprecationmedium impact

React 19 treats ref as a regular prop and deprecates accessing element.ref directly.

Beforetsx
const element = <Input ref={inputRef} />;

const ref = element.ref;
Aftertsx
const element = <Input ref={inputRef} />;

const ref = element.props.ref;
Migration

Read ref from element.props.ref instead of element.ref. Direct element.ref access will be removed from the JSX Element type in a future release.