Breaking Changes
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.
import ReactDOM from "react-dom";
const container = document.getElementById("root");
ReactDOM.render(
<App />,
container,
);
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().
Server-rendered React applications should migrate from the legacy hydrate API to hydrateRoot from react-dom/client.
import { hydrate } from "react-dom";
const container =
document.getElementById("root");
hydrate(
<App />,
container,
);
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 new root.render API does not support the callback argument previously accepted by ReactDOM.render.
ReactDOM.render(
<App />,
container,
() => {
console.log("rendered");
},
);
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.
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 new root API does not support the third callback argument previously accepted by ReactDOM.render.
ReactDOM.render(
<App />,
container,
() => {
console.log("rendered");
},
);
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.
Applications adopting the React 18 root API should unmount through the root object instead of calling unmountComponentAtNode.
import {
unmountComponentAtNode,
} from "react-dom";
unmountComponentAtNode(
container,
);
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.
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.
React 19 removes runtime propTypes checks for function components. Existing propTypes declarations on function components are ignored.
import PropTypes from "prop-types";
function Heading({ text }) {
return <h1>{text}</h1>;
}
Heading.propTypes = {
text: PropTypes.string,
};
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.
React 19 removes defaultProps support from function components. Class components continue to support defaultProps.
function Heading({
text,
}: {
text?: string;
}) {
return <h1>{text}</h1>;
}
Heading.defaultProps = {
text: "Hello",
};
function Heading({
text = "Hello",
}: {
text?: string;
}) {
return <h1>{text}</h1>;
}
Migration Replace function-component defaultProps with JavaScript default parameters.
React 19 removes the legacy contextTypes and getChildContext APIs that were deprecated in React 16.6.
class Parent extends React.Component {
getChildContext() {
return {
theme: "dark",
};
}
render() {
return <Child />;
}
}
Parent.childContextTypes = {
theme: PropTypes.string,
};
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.
React 19 removes string refs from class components. Ref callbacks or createRef should be used instead.
class Form extends React.Component {
componentDidMount() {
this.refs.input.focus();
}
render() {
return (
<input ref="input" />
);
}
}
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.
React 19 removes support for the legacy module pattern factory component style.
function FactoryComponent() {
return {
render() {
return <div>Hello</div>;
},
};
}
function FactoryComponent() {
return <div>Hello</div>;
}
Migration Convert module pattern factories into normal function components.
React 19 removes React.createFactory, an API that predates widespread JSX adoption.
import {
createFactory,
} from "react";
const Button =
createFactory("button");
const element =
Button({
children: "Save",
});
const element = (
<button>
Save
</button>
);
Migration Replace React.createFactory usage with JSX.
React 19 removes the react-test-renderer/shallow entry point. React recommends moving away from shallow rendering.
import ShallowRenderer from
"react-test-renderer/shallow";
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.
React 19 moves act to the React package. The react-dom/test-utils version is deprecated and the other legacy test utilities are removed.
import {
act,
} from "react-dom/test-utils";
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.
React 19 removes ReactDOM.render after its deprecation in React 18. Applications must use createRoot.
import {
render,
} from "react-dom";
render(
<App />,
document.getElementById("root"),
);
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.
React 19 removes the legacy ReactDOM.hydrate API. Server-rendered applications must use hydrateRoot.
import {
hydrate,
} from "react-dom";
hydrate(
<App />,
document.getElementById("root"),
);
import {
hydrateRoot,
} from "react-dom/client";
hydrateRoot(
document.getElementById("root")!,
<App />,
);
Migration Replace ReactDOM.hydrate with hydrateRoot from react-dom/client.
React 19 removes ReactDOM.unmountComponentAtNode. Applications using the root API should unmount through the root instance.
import {
unmountComponentAtNode,
} from "react-dom";
unmountComponentAtNode(
container,
);
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.
React 19 removes findDOMNode. The API was a legacy escape hatch that broke component abstraction and was fragile during refactoring.
import {
findDOMNode,
} from "react-dom";
useEffect(() => {
const input =
findDOMNode(component);
input?.focus();
}, []);
const inputRef =
useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return (
<input ref={inputRef} />
);
Migration Replace findDOMNode with an explicit DOM ref.
New Features
React 18 provides flushSync for rare cases where application code must force React to synchronously flush an update before continuing.
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.
React 18 introduces renderToReadableStream for streaming server rendering in environments that support Web Streams, including modern edge runtimes.
import {
renderToReadableStream,
} from "react-dom/server";
const stream =
await renderToReadableStream(
<App />,
);
React 18 introduces startTransition and useTransition for marking state updates as non-urgent so urgent interactions such as typing can remain responsive.
import {
startTransition,
} from "react";
setInputValue(nextValue);
startTransition(() => {
setSearchQuery(nextValue);
});
useTransition allows components to start non-urgent updates while also exposing whether the transition is pending.
import {
useTransition,
} from "react";
const [
isPending,
startTransition,
] = useTransition();
function selectTab(tab: string) {
startTransition(() => {
setTab(tab);
});
}
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.
function SearchResults({
query,
}: {
query: string;
}) {
return (
<ExpensiveResults query={query} />
);
}
import {
useDeferredValue,
} from "react";
function SearchResults({
query,
}: {
query: string;
}) {
const deferredQuery =
useDeferredValue(query);
return (
<ExpensiveResults
query={deferredQuery}
/>
);
}
React 18 introduces useId for generating stable unique IDs that work across client and server rendering without causing hydration mismatches.
function PasswordField() {
const id = "password-field";
return (
<>
<label htmlFor={id}>
Password
</label>
<input
id={id}
type="password"
/>
</>
);
}
import { useId } from "react";
function PasswordField() {
const id = useId();
return (
<>
<label htmlFor={id}>
Password
</label>
<input
id={id}
type="password"
/>
</>
);
}
React 18 introduces useSyncExternalStore so libraries that integrate external stores can support concurrent rendering with synchronous external-store updates.
useEffect(() => {
return store.subscribe(() => {
setState(store.getSnapshot());
});
}, []);
import {
useSyncExternalStore,
} from "react";
const state = useSyncExternalStore(
store.subscribe,
store.getSnapshot,
store.getServerSnapshot,
);
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.
React 18 adds renderToPipeableStream for Node.js and renderToReadableStream for modern edge runtimes, with streaming Suspense support.
import {
renderToString,
} from "react-dom/server";
const html =
renderToString(<App />);
import {
renderToPipeableStream,
} from "react-dom/server";
const {
pipe,
} = renderToPipeableStream(
<App />,
{
onShellReady() {
pipe(response);
},
},
);
React 19 allows function components to access ref directly as a prop, reducing the need for forwardRef in new function components.
import {
forwardRef,
} from "react";
const Input = forwardRef<
HTMLInputElement,
InputProps
>((props, ref) => {
return (
<input
{...props}
ref={ref}
/>
);
});
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.
React 19 supports returning a cleanup function from a ref callback. React calls the cleanup when the element is removed.
<div
ref={(node) => {
if (node) {
setup(node);
}
}}
/>
<div
ref={(node) => {
setup(node);
return () => {
cleanup(node);
};
}}
/>
React 19 allows <Context> to be used directly as a provider instead of requiring <Context.Provider>.
<ThemeContext.Provider value="dark">
<App />
</ThemeContext.Provider>
<ThemeContext value="dark">
<App />
</ThemeContext>
React 19 introduces Actions for handling asynchronous transitions, including pending states, optimistic updates, errors, and form mutations.
async function submit() {
setPending(true);
try {
await updateName(name);
} finally {
setPending(false);
}
}
const [
isPending,
startTransition,
] = useTransition();
function submit() {
startTransition(async () => {
await updateName(name);
});
}
React 19 introduces useActionState for updating state based on the result of an Action while exposing pending state.
import {
useActionState,
} from "react";
const [
error,
submitAction,
isPending,
] = useActionState(
async (
previousState,
formData,
) => {
const error =
await updateName(
formData.get("name"),
);
return error;
},
null,
);
React 19 introduces useOptimistic for temporarily showing an expected result while an asynchronous Action is in progress.
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);
}
React 19 introduces use for reading resources such as promises and context during rendering. Unlike Hooks, use can be called conditionally.
import {
use,
} from "react";
function Comments({
commentsPromise,
}: {
commentsPromise: Promise<Comment[]>;
}) {
const comments =
use(commentsPromise);
return comments.map(
(comment) => (
<Comment
key={comment.id}
comment={comment}
/>
),
);
}
React 19 extends form action, input formAction, and button formAction props so they can receive functions that handle form submissions.
<form
onSubmit={handleSubmit}
>
<input name="name" />
<button type="submit">
Save
</button>
</form>
async function saveName(
formData: FormData,
) {
await updateName(
formData.get("name"),
);
}
<form action={saveName}>
<input name="name" />
<button type="submit">
Save
</button>
</form>
React 19 adds useFormStatus in react-dom so components can read information about the parent form submission, including whether it is pending.
import {
useFormStatus,
} from "react-dom";
function SubmitButton() {
const {
pending,
} = useFormStatus();
return (
<button
disabled={pending}
type="submit"
>
{pending
? "Saving..."
: "Save"}
</button>
);
}
React 19 adds native support for title, meta, and link metadata tags rendered from components and hoists them to the document head.
function ProductPage({
product,
}: {
product: Product;
}) {
return (
<>
<title>
{product.name}
</title>
<meta
name="description"
content={
product.description
}
/>
<ProductDetails
product={product}
/>
</>
);
}
React 19 adds built-in support for stylesheet links with precedence, allowing React to coordinate stylesheet loading with rendering.
<link
rel="stylesheet"
href="/styles.css"
precedence="default"
/>
React 19 improves support for async scripts rendered anywhere in the component tree by deduplicating and managing them as document resources.
<script
async
src="https://example.com/sdk.js"
/>
Behavior Changes
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.
setTimeout(() => {
setCount((count) => count + 1);
setFlag((flag) => !flag);
// React 17 may render twice.
}, 1000);
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.
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.
useEffect(() => {
const connection =
createConnection();
connection.connect();
// Missing cleanup.
}, []);
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.
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.
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.
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 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.
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.
useEffect(() => {
const socket = connect();
socket.subscribe();
}, []);
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.
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.
React 18 allows components to render undefined. This aligns undefined more closely with other empty render values such as null.
function OptionalContent({
visible,
}: {
visible: boolean;
}) {
if (!visible) {
return null;
}
return <Content />;
}
function OptionalContent({
visible,
}: {
visible: boolean;
}) {
if (!visible) {
return;
}
return <Content />;
}
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.
In React 18, Suspense with fallback={undefined} behaves the same as fallback={null} instead of being ignored.
<Suspense fallback={undefined}>
<LazyPage />
</Suspense>
<Suspense fallback={null}>
<LazyPage />
</Suspense>
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.
fetchData().then(() => {
setCount((count) => count + 1);
setFlag((flag) => !flag);
// React 17:
// two separate renders
});
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.
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.
const root = createRoot(container);
root.render(<App />);
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.