7 min read

React 18: Release Date, Key Features, and What React 19 Changed

Giorgi Giunashvili

Delivery Manager & Software Architect

React 18 was released on March 29, 2022. It was the version that introduced concurrent rendering to React - the foundation that useTransition, useDeferredValue, automatic batching, and streaming server rendering are all built on. Nearly every React feature that has shipped since, up to and including React 19's Actions and Server Components, stands on the concurrency work that landed in this release.

A quick note on where things stand today: the current version of React is 19.2, released in October 2025, and React 19 superseded React 18 in December 2024. React 18 is still widely deployed and works fine, but new features only land in the 19.x line. This article covers what React 18 introduced - still relevant, since these APIs are all part of current React - and then summarizes what changed in React 19 so you can see the full picture. At Redberry we build and maintain production applications with React and React Native alongside our Laravel work, so the notes below reflect how these features behave in real codebases, not just the changelog.

Upgrading to React 18

If you are moving a React 17 project specifically to version 18 (rather than jumping to 19), pin the version explicitly

npm install react@18 react-dom@18
 

Note that npm install react@latest now installs React 19, so the old habit of upgrading via @latest will take you further than one major version.

React 18 introduced a new root API, which requires a small change in your entry point

// Before (React 17)
import ReactDOM from 'react-dom';
import App from './App';

const container = document.getElementById('root');
ReactDOM.render(<App />, container);
// After (React 18)
import { createRoot } from 'react-dom/client';
import App from './App';

const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />);
 

That is the only required change for most applications. React 18 kept breaking changes minimal, though as with any upgrade of a production codebase, run your test suite and check third-party library compatibility before shipping.

Concurrent rendering

The most important addition in React 18 is concurrency. A fundamental property of concurrent React is that rendering is interruptible. Dan Abramov of the React core team explained it with a phone-call analogy: without concurrency, you can have only one phone conversation at a time - if you are talking to Alice and Bob calls, you have to finish with Alice first. With concurrency, you can put Alice on hold, talk to Bob, and switch back. Rendering works the same way: React can pause a low-priority render, handle something urgent, and resume.

React 18 exposed this through new hooks rather than making you manage it directly.

useTransition

By default, all state updates are urgent. useTransition lets you mark some updates as low priority - useful for things like search-as-you-type, where updating the input field must feel instant but re-rendering a large results list can lag behind by a frame or two without anyone noticing.

import { useState, useTransition } from 'react';

function Search({ items }) {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState(items);
  const [isPending, startTransition] = useTransition();

  function handleChange(e) {
    setQuery(e.target.value); // urgent: keep the input responsive

    startTransition(() => {
      // non-urgent: can be interrupted by further typing
      setResults(filterItems(items, e.target.value));
    });
  }

  return (
    <>
      <input value={query} onChange={handleChange} />
      {isPending ? <Spinner /> : <Results items={results} />}
    </>
  );
}
 

The hook returns two values: an isPending boolean for showing a loading state while the transition is active, and a startTransition function that wraps the updates you want to deprioritize. In practice this is the difference between a search box that stutters on every keystroke and one that stays smooth while the heavy list catches up.

useDeferredValue

useDeferredValue has broadly the same effect as useTransition, but works on values instead of state updates. It accepts a value and returns a copy that lags behind during urgent work. Reach for it when you don't control the state update itself - for example, when the value comes down as a prop.

import { useDeferredValue } from 'react';

function Results({ query }) {
  const deferredQuery = useDeferredValue(query);
  // Heavy rendering keyed off deferredQuery stays a step behind
  // the urgent input updates instead of blocking them.
  return <ExpensiveList query={deferredQuery} />;
}
 

Automatic batching

Before React 18, multiple state updates were only batched into a single re-render inside React event handlers. Updates inside promises, setTimeout callbacks, or native event handlers each triggered their own re-render. React 18 batches all of them automatically:

setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 18: one re-render for both updates.
  // React 17: two separate re-renders.
}, 1000);
 

Some libraries previously used the undocumented unstable_batchedUpdates API to force this behavior. With React 18, that workaround became unnecessary.

Strict Mode and double-running effects

If you use useEffect with an empty dependency array in React 18, you may be surprised to see it run twice in development. This is intentional: Strict Mode simulates unmounting and remounting each component in development to surface effects that don't clean up after themselves - preparation for reusable state features in later versions.

Removing Strict Mode makes the symptom go away, but we wouldn't recommend it - it's flagging real bugs. The right fix is a cleanup function that makes the effect safe to run twice:

useEffect(() => {
  let ignore = false;

  fetchData().then((data) => {
    if (!ignore) setData(data);
  });

  return () => {
    ignore = true;
  };
}, []);
 

An extra fetch in development mode is harmless, and production behavior is unchanged. For anything beyond trivial cases, data-fetching libraries like TanStack Query or SWR handle caching, deduplication, and race conditions for you - that's what we use on client projects rather than hand-rolling fetch logic in effects.

What React 19 changed

When this article was first written, Suspense-based data fetching in React 18 was real but not fully usable outside frameworks - that was the one big gap in the concurrency story. React 19, released on December 5, 2024, is the version that closed it, along with a set of changes that remove long-standing boilerplate:

  • The use() API - components can now read promises and context directly, with Suspense handling the loading state. This is the first-class data-fetching answer React 18 didn't have.
  • Actions - async functions wired into forms and transitions, with useActionState and useOptimistic managing pending states, errors, and optimistic UI that previously took manual state juggling.
  • Server Components - stable in 19, letting parts of the tree render ahead of time on the server (framework support required, e.g. Next.js).
  • ref as a regular prop - function components no longer need forwardRef.
  • Native document metadata - <title> and <meta> tags can be rendered directly in components, with React hoisting them into <head>.

The 19.x line has kept moving since: React 19.1 arrived in June 2025, and React 19.2 - the current release - shipped in October 2025 with the <Activity /> component for pre-rendering hidden UI and the useEffectEvent hook.

The practical takeaway: everything React 18 introduced still works and still matters, because React 19 is built on the same concurrent foundation. If you're starting a new project, start on React 19. If you're maintaining a React 18 application, there's no forced deadline - React has no formal end-of-life schedule the way backend frameworks do - but new capabilities will only land in 19.x, so the upgrade is worth planning rather than deferring indefinitely.

If you're weighing that upgrade or building something new on React, frontend work is part of what we do daily at Redberry across React and React Native alongside our Laravel practice - feel free to explore our software development services or get in touch.

Updated on by

Giorgi Giunashvili

About the author

Giorgi Giunashvili

Delivery Manager at Redberry

Co-founded Redberry's Laravel Bootcamp

Built E-space, EV Marketplace

Giorgi is a Software Architect and Laravel engineer at Redberry who has delivered production platforms across EV mobility, banking, car rental, and professional-services SaaS. He built E-space, an EV-charger marketplace, the ProCredit Bank website, and Skippit, a multi-tenant ERP for professional-service firms. He also co-founded Redberry's Laravel bootcamp and authors the company's PHP and Laravel developer tutorials.

Trending News

Jun
22

Hosting the First Official Laravel Meetup in Georgia

We hosted the first official Laravel Meetup in Georgia, bringing together more than 100 attendees for an evening dedicated to Laravel, engineering, and community.

Jun
22

Hosting a RDBR Meetup on Our Tavistock Protect Partnership

At our latest RDBR Meetup, we looked inside our two-year partnership with Tavistock Protect and the product we have been building together: PP Mobius.

img

Meet the authors

We are a 200+ people agency and provide product design, software development, and creative growth marketing services to companies ranging from fresh startups to established enterprises. Our work has earned us 100+ international awards, partnerships with Laravel, Vue, Meta, and Google, and the title of Georgia’s agency of the year in 2019 and 2021.

CONTACT US
img

Get in touch

Dati Chkhikvishvili

Chief Business Officer