React's State of the Art: Stable, Experimental, and Deprecated APIs

Understand which React APIs are stable and safe to rely on today, which are version-dependent or experimental, and which are deprecated or removed — and see how every Modern/Legacy/Advanced/Ecosystem label used throughout this course maps onto the specific React and TypeScript versions the course has targeted.

React has evolved substantially since its early class-component days, and it keeps evolving — hooks, concurrent rendering, Server Components, the React Compiler, and the use() API each arrived in a different release and at a different level of stability. Knowing what's stable versus experimental isn't trivia; it directly determines what you can safely ship in a production app today versus what you should watch but not yet depend on. This lesson is the course's own map of that landscape, tied to the exact versions everything you've learned has been built against.

The versions this course targets

Throughout this course, every example, every API, and every piece of guidance has targeted React 18 and React 19 as the baseline, with TypeScript 5.x for all typed examples. Where a feature differs between React 18 and React 19, this lesson calls it out explicitly — otherwise, assume any example you've seen works identically on both.

This matters because React's own documentation and the wider ecosystem contain a huge amount of content written for React 16 and 17 (the class-component and early-hooks era), and treating that older content as equally current can lead you to write deprecated or removed code without realizing it — exactly the gap Module EQ (Legacy React) exists to close.

TypeScript 5.x brought a more capable type system (const type parameters, the satisfies operator, decorators, improved inference) that the typed examples throughout this course lean on. Older TypeScript (4.x and earlier) can run much of this course's JavaScript-equivalent code but will reject some of the more modern typed patterns shown.

Stable APIs — safe to build on today

Stable APIs are the ones with a long-term commitment from the React team: they work identically across React 18 and 19, have no deprecation warning, and are what production code should be written with by default. This is the large majority of what this course has taught.

This includes: function components; the core hooks (useState, useEffect, useContext, useRef, useReducer, useMemo, useCallback, useLayoutEffect); createContext/Provider/useContext; the Context API generally; error boundaries (still class-component-only as of React 19 — there is no hook equivalent); portals (createPortal); StrictMode; and createRoot as the standard mounting API since React 18. React Router (the most widely used routing library) and the broader hooks-based ecosystem convention this course has used throughout are equally treated as the stable, default way to build.

• Stable: useState, useEffect, useContext, useRef, useReducer, useMemo, useCallback, useLayoutEffect • Stable: createContext, Context.Provider, createPortal, StrictMode • Stable: createRoot (react-dom/client) — the required mounting API since React 18 • Stable: error boundaries (class-component API only; no function-component equivalent exists as of React 19)

Version-dependent APIs — behavior or availability differs by release

Version-dependent APIs exist and are stable, but their exact behavior, defaults, or even presence changed between React 18 and React 19 specifically — code relying on them must be version-aware.

Automatic batching (React groups multiple state updates into a single re-render) became the default everywhere in React 18 — including inside promises, timeouts, and native event handlers — where React 17 only batched inside React's own event handlers. useId (for generating unique, SSR-safe IDs) and useTransition/useDeferredValue (for marking updates as low-priority) arrived in React 18 as part of its concurrent rendering features and don't exist on React 17. React 19 changed how ref is passed to function components — ref can now be read as a normal prop, removing the need for forwardRef in the common case, while forwardRef itself remains supported for backward compatibility but is expected to be deprecated in a future release.

• Automatic batching — the default in React 18+; was inconsistent/manual-triggered in React 17 • useId, useTransition, useDeferredValue — introduced in React 18, absent in React 17 • ref as a plain prop on function components — new in React 19; forwardRef still works but is the older pattern now • useEffect timing and Strict Mode's double-invoking of effects in development — tightened in React 18 specifically to surface effect cleanup bugs

Framework-specific APIs — not part of React core

Some capabilities described as 'React features' in blog posts and tutorials are actually implemented by a specific framework built on top of React, not by the react/react-dom packages themselves. Using them assumes you've adopted that framework and its conventions.

React Server Components (RSC) — components that render on the server and never ship their JavaScript to the client — are a React-core-adjacent capability, but require a framework (a Next.js-style full-stack React framework) with a compatible bundler and server runtime to actually use; plain react/react-dom alone does not give you RSC. File-based routing, built-in image/font optimization, and server actions similarly belong to specific frameworks, not to React itself.

This course has consistently distinguished 'core React' (what react and react-dom provide on their own) from framework-layered features, exactly so you know which parts of what you've learned travel with you into any React setup, and which parts are tied to a specific framework choice.

Experimental APIs — real, but not yet a safe default

Experimental APIs are ones the React team has shipped in some form and is actively developing, but that either require an experimental release channel, are new enough that ecosystem tooling hasn't fully caught up, or carry an explicit 'this may still change' caveat from the React team. Treat these as worth understanding and watching, not as defaults for a new production feature.

The use() API lets you read the value of a promise or context directly during render (including conditionally, unlike hooks) — it's shipped and usable in React 19 but is most powerful combined with Server Components and Suspense-based data fetching patterns that are still settling across the ecosystem. Actions (functions passed to <form action={fn}> or triggered via useActionState/useFormStatus) are a React 19 addition for handling pending/error/optimistic state around async form submissions, and are newer, with fewer battle-tested large-scale examples than the core hooks. The React Compiler (an opt-in build-time tool that automatically memoizes components and values, aiming to reduce how often you need useMemo/useCallback by hand) is real and usable but was still being adopted incrementally by production codebases as of this course's writing — treat manual useMemo/useCallback as the still-necessary default until you've specifically opted a project into the compiler and verified its output.

• use() — reads a promise or context during render, including conditionally; newest and most Suspense/RSC-coupled • Actions (form action={fn}, useActionState, useFormStatus) — React 19, async form-submission state handling • React Compiler — opt-in automatic memoization; still an incremental adoption story across the ecosystem • Experimental status can and does change between minor releases — recheck before depending on one in a real project

Deprecated and removed APIs

Deprecated means still present but discouraged, usually with a console warning, and planned for removal. Removed means the API no longer exists at all in current React and code using it will error or fail to compile — not merely warn.

String refs and legacy context (contextTypes/childContextTypes/getChildContext) are removed from current React entirely. ReactDOM.render and ReactDOM.hydrate were deprecated in React 18 in favor of createRoot/hydrateRoot from react-dom/client, and removed in React 19. componentWillMount, componentWillReceiveProps, and componentWillUpdate (the original, un-prefixed lifecycle names) were deprecated years ago in favor of their UNSAFE_-prefixed equivalents (UNSAFE_componentWillMount, etc.) specifically to make remaining usages searchable and to discourage new use, since these methods are unsafe to use with concurrent rendering features.

PropTypes (runtime prop validation via the prop-types package) still functions but has been effectively superseded by TypeScript's compile-time prop checking throughout this course — TypeScript interfaces/types for props are what this course has used throughout, treating PropTypes as a legacy JavaScript-only-codebase pattern rather than the modern default.

• Removed: string refs, legacy context (contextTypes/childContextTypes) • Removed in React 19 (deprecated in 18): ReactDOM.render, ReactDOM.hydrate • Deprecated (UNSAFE_ prefix required): componentWillMount, componentWillReceiveProps, componentWillUpdate • Superseded (not removed, but not this course's default): PropTypes, in favor of TypeScript prop types

How this course has labeled features throughout

Every module in this course has used a consistent set of labels, and this lesson is the key that ties them together: 'Modern' means a stable, current API this course teaches as the default (hooks, function components, createRoot). 'Legacy' means a pattern you need to recognize in existing code but should not write new (class lifecycle methods, HOCs, render props, string refs, ReactDOM.render) — covered in depth in Module EQ. 'Advanced' has meant stable-but-more-specialized APIs that most components don't need but real applications eventually do (useImperativeHandle, useLayoutEffect, custom hook composition, performance-focused memoization). 'Ecosystem' has meant capability that comes from a widely-adopted library rather than react/react-dom itself (React Router, state management libraries, form libraries, framework-specific features like Server Components). 'Version-specific/Experimental' — this lesson's focus — means tied to a particular release or still stabilizing, and is flagged explicitly wherever it appears rather than presented as an unconditional default.

Reading a feature's label, together with this lesson's version matrix, tells you everything you need to decide how much to lean on it: is it safe for a new production feature today (Modern, Advanced, most of Ecosystem), does it need recognition but not new usage (Legacy), or does it need a version check and a measured amount of caution before you commit to it (Version-specific/Experimental)?

React feature matrix — stability by category

This matrix is the single reference point for every Modern/Legacy/Advanced/Ecosystem/Version-specific label used across the whole React course. 'Experimental' here means real and shippable in React 19, not a proposal — but newer, less battle-tested, and worth a version/tooling check before depending on it in production. Always re-verify against the React changelog for the exact version you're targeting — this matrix reflects the React 18/19 + TypeScript 5.x baseline this course has used throughout.

// Stable (React 18 & 19 — default choice for new code)
useState, useEffect, useContext, useRef, useReducer,
useMemo, useCallback, useLayoutEffect,
createContext / Provider, createPortal, StrictMode,
createRoot (react-dom/client), error boundaries (class-only)

// Version-dependent (behavior/availability differs 18 vs 19)
Automatic batching            -> default since React 18
useId, useTransition,
useDeferredValue               -> introduced in React 18
ref as a plain function-component prop -> new in React 19 (forwardRef still works)

// Framework-specific (needs a full-stack React framework, not react/react-dom alone)
React Server Components (RSC), file-based routing,
server actions, built-in image/font optimization

// Experimental / newest (usable, still stabilizing across the ecosystem)
use()                           -> React 19; reads promises/context during render
Actions, useActionState,
useFormStatus                   -> React 19; async form submission state
React Compiler                  -> opt-in automatic memoization; incremental adoption

// Deprecated / removed (do not write new code with these)
ReactDOM.render / hydrate       -> removed in React 19 (deprecated in 
…

Code Examples

This demonstrates a version-dependent API: automatic batching. On React 17, state updates queued inside a native addEventListener callback (as opposed to React's own synthetic onClick) would each trigger a separate re-render. On React 18 and 19, all React state updates are batched into a single re-render regardless of where they originate, which is both a performance win and a behavior change that could break code written to assume React 17's more limited batching. Expected result: Component re-renders once per click on React 18/19; would have re-rendered twice on React 17 for this native-listener case. If you inherit code with manual workarounds for 'unbatched' native-event updates (e.g. manual flushSync calls that seem unnecessary), that's often a sign the code predates React 18's automatic batching and can likely be simplified.

setCount(c => c + 1); setFlag(f => !f); // batched into one render on React 18+ use() is a genuinely newer capability — unlike other hooks, it can be called conditionally and reads a promise's eventual value directly, integrating tightly with Suspense. It's usable in React 19 today, but this course has taught the useEffect + useState pattern as the stable default throughout, precisely because it works identically across React 18 and 19 and doesn't assume a framework wired up for Suspense-based data fetching the way use() often does in practice. Expected result: Both eventually render the user's name; the use()-based version requires React 19 and a Suspense boundary, the hook-based version runs unmodified on React 18 or 19.

const user = use(userPromise); // can be called conditionally, unlike other hooks

Common mistake: Reaching for use() as a drop-in replacement for useEffect-based fetching in a codebase that isn't already using Suspense boundaries for loading states — without that scaffolding in place, use() suspends in a way the surrounding tree isn't set up to handle.

function ClickCounter() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function handleClick() {
    // In a native DOM event listener added via addEventListener,
    // React 17 would NOT batch these into one re-render — two renders occurred.
    // React 18+ batches automatically everywhere, including here — one re-render.
    setCount(c => c + 1);
    setFlag(f => !f);
    console.log('Still same render so far in React 18+');
  }

  useEffect(() => {
    const button = document.getElementById('inc');
    button?.addEventListener('click', handleClick);
    return () => button?.removeEventListener('click', handleClick);
  }, []);

  return <button id="inc">{count} / {String(flag)}</button>;
}
// Newer (React 19): use() reads a promise directly during render
import { use, Suspense } from 'react';

function UserName({ userPromise }: { userPromise: Promise<{ name: string }> }) {
  const user = use(userPromise); // suspends until the promise resolves
  return <span>{user.name}</span>;
}

function Profile({ userPromise }: { userPromise: Promise<{ name: string }> }) {
  return (
    <Suspense fallback={<span>Loading...</span>}>
      <UserName userPromise={userPromise} />
    </Suspense>
  );
}

// Stable, version-agnostic equivalent used throughout most of this course:
function useUser(id: string) {
  const [user, setUser] = useState<{ name: string } | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let cancelled = false;
    fetch(`/api/users/${id}`).then(res => res.json()).then(data => {
      if (!cancelled) { setUser(data); setLoading(false); }
    });
    return () => { cancelled = true; };
  }, [id]);

  return { user, loading };
}

React API stability spectrum, React 18/19 + TypeScript 5.x baseline

Moving left to right: never write new code on the left column; the middle-left is fine to read but not to author; the stable column is this course's default; the right two columns need a version check or framework check before you depend on them in production.

REMOVED / DEPRECATED        LEGACY (recognize only)     STABLE (default)          VERSION-DEPENDENT        EXPERIMENTAL / NEWEST
---------------------        ------------------------    ------------------        -------------------      -----------------------
String refs                  Class lifecycle methods      useState / useEffect       Automatic batching        use()
Legacy context                HOCs, render props           useContext / useRef        useId / useTransition     Actions, useActionState
ReactDOM.render (removed 19)  ReactDOM.render (see legacy) createRoot                 ref-as-prop (React 19)    React Compiler
UNSAFE_ lifecycle names        PropTypes                    Context API, portals       useDeferredValue          RSC (framework-specific)

Important Rules

Default to the Stable column (hooks, function components, createRoot, Context API) for any new production code — this is what the whole course has taught as the baseline.

Treat 'Framework-specific' features (Server Components, file-based routing, server actions) as requiring a specific framework — they are not available from react/react-dom alone.

Treat Experimental/newest APIs (use(), Actions, React Compiler) as worth learning now but requiring an explicit version and tooling check before shipping — verify against the current React changelog for your target version.

Never write new code using removed APIs (string refs, legacy context, ReactDOM.render on React 19) — these will error or fail outright, not merely warn.

When a lifecycle method appears with an UNSAFE_ prefix (UNSAFE_componentWillMount, etc.), treat it as a deprecation signal, not a normal API to reach for.

This course's TypeScript examples assume TypeScript 5.x; older TypeScript versions may reject some of the more modern typed patterns shown (satisfies, const type parameters).

Why this matters

React's ecosystem moves fast enough that a two-year-old blog post, Stack Overflow answer, or even official-looking tutorial can teach an API that's since been deprecated, or omit one that's since become standard. Being able to independently classify 'is this stable, version-dependent, framework-specific, experimental, or dead' is what lets you evaluate new information on your own once this course ends, rather than needing every future React change explained to you from scratch. This lesson is deliberately the course's last standalone concept lesson for exactly that reason — everything else you build from here (the practical projects and the capstone) rests on the stable column of this matrix.

Assuming every hook or API you read about in a React tutorial is safe to use in any React version without checking.

Check whether the API is Stable (works everywhere from React 18 on), Version-dependent (behavior differs 18 vs 19), or Experimental (newest, React 19+, still stabilizing) before depending on it.

useId, useTransition, and useDeferredValue simply don't exist on React 17; use() and Actions require React 19. Treating tutorial content as universally applicable across versions is a common source of confusing 'this hook doesn't exist' errors.

Reaching for the React Compiler's automatic memoization as a reason to skip understanding useMemo/useCallback.

Learn useMemo/useCallback as the stable, version-agnostic default; treat the React Compiler as an opt-in optimization to adopt deliberately on a project, not a reason to skip the fundamentals.

The Compiler was still an incremental, opt-in adoption story across the ecosystem as of this course's writing — most real codebases you'll work in still rely on manual memoization, and you need to be able to read and write it.

Treating Server Components (RSC) as something available in any React app just by importing from react.

Recognize RSC as framework-specific — it requires a compatible full-stack React framework with a server runtime and bundler that understands it; plain react/react-dom does not provide it.

Conflating 'part of React' with 'part of a framework built on React' leads to trying to use RSC syntax in a plain Vite + react-dom SPA, where it simply has no meaning.

[conceptual] This course has targeted which React and TypeScript versions as its baseline? A. React 16/17 and TypeScript 3.x B. React 18/19 and TypeScript 5.x C. React 15 and plain JavaScript only, with no TypeScript D. A single unspecified 'latest' version that changes without notice

B. React 18/19 and TypeScript 5.x — The course has consistently targeted React 18 and 19 as its baseline, with TypeScript 5.x for typed examples, and calls out explicitly wherever a feature differs between React 18 and 19.

[conceptual] Why can't a plain Vite + react-dom single-page app use React Server Components just by importing something from the react package? A. RSC is a framework-specific capability — it requires a full-stack React framework with a compatible bundler and server runtime, not just react/react-dom B. Server Components were removed from React entirely and no longer exist C. RSC only works with class components, not function components D. RSC requires TypeScript 4.x specifically and fails on TypeScript 5.x

A. RSC is a framework-specific capability — it requires a full-stack React framework with a compatible bundler and server runtime, not just react/react-dom — React Server Components require a framework layer (bundler + server runtime support) beyond what react and react-dom alone provide — this is exactly why this lesson categorizes RSC as framework-specific rather than core-stable.

[recognition] A component calls use() to read a promise conditionally, with no Suspense boundary anywhere in its ancestor tree. What's the concern? A. There is no concern; use() has no requirements beyond being called inside a component B. use() suspends the component until the promise resolves, and without a Suspense ancestor there's no fallback UI defined, likely breaking the render C. use() is fully removed from React and this code cannot compile at all D. use() only works inside class components, never function components

B. use() suspends the component until the promise resolves, and without a Suspense ancestor there's no fallback UI defined, likely breaking the render — use() is an experimental-adjacent, React 19 API that suspends rendering until its promise resolves; it depends on a Suspense boundary somewhere above it to render a fallback during that suspended state, which this example is missing.

[application] Which of the following correctly separates a Deprecated/removed API from a Version-dependent one? A. ReactDOM.render (removed in React 19) is deprecated/removed; automatic batching (default behavior change starting in React 18) is version-dependent B. useState is deprecated; useEffect is version-dependent C. Both ReactDOM.render and automatic batching are experimental APIs D. There is no meaningful distinction between these categories

A. ReactDOM.render (removed in React 19) is deprecated/removed; automatic batching (default behavior change starting in React 18) is version-dependent — ReactDOM.render was deprecated in React 18 and removed outright in React 19 — code using it will not run on React 19. Automatic batching is a stable, real behavior that simply differs by version (limited in React 17, full and automatic from React 18 onward) — it isn't going away, its scope just changed.

You're reviewing a pull request for a new production feature. It imports use from 'react' to read a promise conditionally inside a component that has no Suspense boundary anywhere in its parent tree, and it also defines componentWillReceiveProps inside a class component with no UNSAFE_ prefix. Classify each choice using this lesson's categories, and say what you'd ask the author to change.

use() without a Suspense boundary: this is an Experimental/newest-category API being used without the scaffolding it depends on — use() suspends the component until its promise resolves, and without a <Suspense> ancestor, there's no fallback UI defined for that suspended state, likely producing a broken or blank render. Ask the author to either add a Suspense boundary or fall back to the stable useEffect + useState pattern used elsewhere in the codebase. componentWillReceiveProps without the UNSAFE_ prefix: this is a Deprecated API being used in its old, unsafe form — the un-prefixed name has been deprecated for years specifically because it's unsafe with concurrent rendering. Ask the author to replace it with the equivalent useEffect logic in a function component (this course's Legacy → Modern mapping) rather than merely renaming it to UNSAFE_componentWillReceiveProps, which only silences the warning without fixing the underlying issue.

One of these is a stability/readiness concern about a newer API used without its required scaffolding; the other is a deprecated API that may not even run depending on the React version.