React Upgrade Guide

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

30Changes
6Breaking
14Actions
9Features
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.

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);
    },
  },
);

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.

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.

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.