React Upgrade Guide

React 18 โ€” What’s New

React 18 introduced a new concurrent rendering engine without breaking existing apps. Here are the most important changes.

New Root API

The old ReactDOM.render() is deprecated. Replace it with createRoot():

// โŒ Old (React 17 and below)
import ReactDOM from "react-dom";
ReactDOM.render(<App />, document.getElementById("root"));

// โœ… New (React 18+)
import ReactDOM from "react-dom/client";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

Automatic Batching

In React 17, state updates inside event handlers were batched, but updates inside setTimeout, Promises, or native events were not. React 18 batches all updates automatically.

// React 18: both setCount and setFlag are batched into ONE re-render
setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // Only one re-render โ€” automatic batching!
}, 1000);

Transitions

Mark non-urgent state updates as transitions so React can deprioritise them and keep the UI responsive.

import { startTransition } from "react";

function SearchPage() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);

  function handleInput(e) {
    // Urgent: update the input immediately
    setQuery(e.target.value);

    // Non-urgent: defer the heavy search results update
    startTransition(() => {
      setResults(searchDatabase(e.target.value));
    });
  }

  return <input value={query} onChange={handleInput} />;
}

Suspense for Data Fetching

React 18 extends Suspense to work with data fetching frameworks (Next.js, Relay, React Query). The component renders a fallback while data loads.

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <UserProfile />  {/* Shows <Spinner /> until UserProfile's data is ready */}
    </Suspense>
  );
}
NoteUpgrading from React 17 to 18 requires only two changes: update react and react-dom to ^18.0.0, and switch to the new createRoot API.