Upgrading React 18 with TypeScript: A Complete Guide

Master the React 18 upgrade with TypeScript--migrate rendering APIs, leverage automatic batching, and unlock concurrent features for better performance.

Modern web development demands performant, type-safe applications. React 18 introduces groundbreaking concurrent features that transform how applications handle state updates and rendering. This guide walks you through upgrading your React TypeScript projects to leverage these improvements while maintaining code quality and preventing regressions.

React 18 represents a paradigm shift in how React handles rendering and state updates. The introduction of concurrent rendering enables applications to remain responsive even during intensive updates, creating smoother user experiences. For TypeScript projects specifically, the updated type definitions catch more potential issues at compile time, reducing runtime errors and improving developer productivity.

The upgrade positions your codebase for future capabilities, ensuring compatibility with modern libraries and tooling that assume React 18 features. Our web development team specializes in React migrations and can help guide your application through a smooth transition.

Why Upgrade to React 18

Key benefits that make the upgrade worthwhile

Concurrent Rendering

Applications remain responsive during intensive updates through React's new concurrent renderer.

Automatic Batching

State updates are grouped more efficiently, reducing unnecessary re-renders across all contexts.

Stricter TypeScript Types

Updated type definitions catch more issues at compile time, improving code quality.

Streaming SSR

Improved server-side rendering delivers content to users faster with Suspense support.

Installing React 18

The installation process begins with updating your package.json dependencies. Run the appropriate command for your package manager:

npm install react@latest react-dom@latest

For projects using TypeScript, update your type definitions to match:

npm install @types/react@latest @types/react-dom@latest

The latest versions align with React 18's type system, which includes stricter type checking and explicit children prop handling. After installation, run your TypeScript compiler to identify any type errors that need addressing.

Verify your build tooling supports React 18. Most modern bundlers including Webpack 5 and Vite work without configuration changes, but ensure you're using recent versions. Check that your Babel configuration, if you use one, includes the latest React preset. For teams using Webpack for module bundling, our guide on transpiling ES modules with Webpack covers advanced configuration scenarios.

Migrating Client Rendering APIs

React 18 introduces a new root API that replaces the legacy ReactDOM.render approach. This change enables concurrent features and provides better ergonomics for managing the React root.

The Old Approach

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(
 <React.StrictMode>
 <App />
 </React.StrictMode>,
 document.getElementById('root')
);

The New Approach

import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

const container = document.getElementById('root');
if (container) {
 const root = createRoot(container);
 root.render(
 <React.StrictMode>
 <App />
 </React.StrictMode>
 );
}

Note several important changes: createRoot is imported from react-dom/client, accepts the DOM element directly, and the callback parameter has been removed. If your application uses React Router for routing, refer to our guide on React Router v6 and future routing approaches for migration considerations.

Understanding TypeScript Definition Changes

React 18 brings significant changes to TypeScript definitions that improve type safety but require code adjustments. For developers familiar with statically typed languages, understanding these changes strengthens your overall TypeScript proficiency. The patterns used in React 18 align with industry best practices for type-safe component design.

Explicit Children Prop

The most notable change involves the children prop, which is no longer implicitly included in component props types:

import { ReactNode } from 'react';

interface ButtonProps {
 children: ReactNode;
 onClick: () => void;
 variant: 'primary' | 'secondary';
}

This explicit approach improves code clarity and catches issues where components don't accept the children you attempt to pass. If you're coming from C# or another statically typed language, you'll appreciate how these TypeScript patterns for typed languages translate to React development.

Event Handler Types

Event handler types have been refined with more specific types:

function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
 console.log(event.target.value);
}

function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
 console.log(event.currentTarget);
}

useRef Generic Types

const inputRef = useRef<HTMLInputElement>(null);
const divRef = useRef<HTMLDivElement>(null);

forwardRef Components

const MyInput = forwardRef<HTMLInputElement, InputProps>(
 ({ label, ...props }, ref) => {
 return (
 <div>
 <label>{label}</label>
 <input ref={ref} {...props} />
 </div>
 );
 }
);

Automatic Batching Improvements

React 18 introduces automatic batching that extends beyond React event handlers to all updates, including those in setTimeout, Promises, and native event handlers.

Before React 18

Updates inside setTimeout were not batched, triggering separate re-renders.

With React 18

All updates are automatically batched into a single re-render, regardless of where they originate from.

Opting Out with flushSync

import { flushSync } from 'react-dom';

function handleClick() {
 flushSync(() => {
 setCount(c => c + 1);
 });
 // DOM is now updated
 console.log(document.getElementById('counter')?.textContent);
}

Use flushSync sparingly, as it defeats the performance benefits of automatic batching.

Concurrent Features: startTransition and useDeferredValue

React 18 introduces concurrent features that help applications remain responsive during intensive updates.

startTransition

import { useState, startTransition } from 'react';

function SearchResults({ query }) {
 const [results, setResults] = useState([]);

 function handleChange(event) {
 const value = event.target.value;
 startTransition(() => {
 setResults(searchDatabase(value));
 });
 }

 return (
 <div>
 <input onChange={handleChange} />
 <ResultsList results={results} />
 </div>
 );
}

Updates inside startTransition are treated as lower priority, keeping the interface responsive during rapid changes.

useDeferredValue

import { useState, useDeferredValue } from 'react';

function SearchResults({ query }) {
 const deferredQuery = useDeferredValue(query);

 return (
 <div>
 <input value={query} onChange={e => setQuery(e.target.value)} />
 <ExpensiveList query={deferredQuery} />
 </div>
 );
}

The ExpensiveList receives the deferred query value, which lags during rapid changes to prevent blocking input responsiveness.

Strict Mode Behavior Changes

React 18's Strict Mode introduces additional development-only checks that help identify components not resilient to mounting and unmounting.

In development mode, components using Strict Mode will mount, unmount, and remount immediately, revealing issues with:

  • Missing cleanup in useEffect: Effects that don't return a cleanup function
  • Effects that depend on single mounting: Code assuming effects run only once
  • Subscription issues: Subscriptions not properly cleaned up

Proper Effect Cleanup

useEffect(() => {
 const subscription = dataSource.subscribe(handleChange);
 return () => {
 dataSource.unsubscribe(subscription);
 };
}, [dataSource]);

Address these issues systematically and restore Strict Mode afterward, as the checks catch real bugs that could affect users in edge cases.

Testing with React 18

React 18's testing patterns have evolved, particularly around the act() API. Comprehensive testing ensures your upgrade doesn't introduce regressions. For Next.js applications with TypeScript, our guide on end-to-end testing with Cypress covers integration testing strategies that work well with React 18.

Upgrade Testing Library

npm install @testing-library/react@^14.0.0

Basic Test Example

import { render, screen, fireEvent } from '@testing-library/react';

test('button click updates counter', () => {
 render(<Counter />);
 const button = screen.getByRole('button', { name: /increment/i });
 fireEvent.click(button);
 expect(screen.getByText('1')).toBeInTheDocument();
});

Tests should work without manual wrapping in act() for most scenarios. The act() function is now imported from @testing-library/react rather than react-dom/test-utils.

Common Migration Issues and Solutions

Type Errors with Children Prop

Add children: React.ReactNode to your component interfaces where needed.

Legacy Context API Usage

Update patterns using the legacy context API to work better with concurrent features.

Custom Render Implementations

Update custom render implementations to use createRoot and hydrateRoot APIs.

setTimeout/setInterval in Effects

Ensure proper cleanup with clearTimeout or clearInterval to handle Strict Mode's behavior.

Third-Party Component Compatibility

Verify all third-party components support React 18 by checking for updates from maintainers.

If you encounter complex issues during migration, our full-stack development team can assist with code reviews and migration planning.

Conclusion

Upgrading to React 18 with TypeScript brings significant benefits including improved performance, better type safety, and access to concurrent features. The migration process involves updating dependencies, migrating rendering APIs, addressing TypeScript definition changes, and adapting testing patterns.

The investment in this upgrade positions your application for future capabilities and modern best practices. Take advantage of automatic batching, concurrent features, and improved Strict Mode behavior to deliver better user experiences.

Start by assessing your current project state, create a feature flag for the upgrade work, and proceed systematically through the migration steps. Test thoroughly and address issues as they arise for a smooth transition to React 18.

For organizations seeking ongoing support, our technology consulting services can provide strategic guidance on React upgrades and modern web architecture decisions.

Frequently Asked Questions

Ready to Modernize Your React Application?

Our team specializes in React upgrades, performance optimization, and modern web development. Let's discuss how we can help your application leverage React 18's capabilities.

Sources

  1. React.dev - How to Upgrade to React 18 - Official migration guide covering all technical aspects
  2. React.dev - React 18 Release Post - Release announcement with feature overview
  3. DefinitelyTyped - React 18 Typings PR - TypeScript definition changes
  4. React Working Group - Automatic Batching - New external store API documentation