Overview of React 18
React 18 is a significant update resulting from years of research and development by the Meta (formerly Facebook) team. Officially released on March 29, 2022, this version includes important changes in the foundations of React.
React 18 not only brings new features but also introduces an architectural change that fundamentally alters how React works: Concurrent Rendering.
In previous versions of React, the rendering process occurred uninterrupted and synchronously. Once a render started, the user interface could be blocked until it was completed. Concurrent Rendering, which comes with React 18, allows rendering processes to be interruptible, pausable, and even cancelable. This approach significantly improves the user experience.
Automatic Batching
One of the most useful improvements in React 18 is the Automatic Batching feature. In older versions, React could only group state updates within its own event handlers. With React 18, all state updates - whether they are inside Promises, setTimeout, native event handlers, or any other handler - are automatically grouped.
Let's explain with a simple example:
// In React 17
setTimeout(() => {
setCount(c => c + 1); // Render triggered
setFlag(f => !f); // Render triggered
}, 1000);
// In React 18
setTimeout(() => {
setCount(c => c + 1); // Render not triggered
setFlag(f => !f); // A single render triggered for both updates
}, 1000);createRoot API
React 18 introduces a new root API for rendering applications. This new API is required to enable concurrent features:
// React 17
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));
// React 18
import ReactDOM from 'react-dom/client';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);useTransition Hook and startTransition API
One of the most important innovations of React 18 is that it offers new APIs to manage transitions. These APIs allow distinguishing between urgent updates and non-urgent updates.
The useTransition hook is used to manage large rendering processes while maintaining the responsiveness of the user interface:
import { useTransition } from 'react';
function SearchComponent() {
const [searchQuery, setSearchQuery] = useState('');
const [searchResults, setSearchResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleSearch = (e) => {
// Urgent update (updates the input value immediately)
setSearchQuery(e.target.value);
// Non-urgent update (calculating results may take time)
startTransition(() => {
// Heavy computation process
const results = computeSearchResults(e.target.value);
setSearchResults(results);
});
};
return (
<div>
<input type="text" value={searchQuery} onChange={handleSearch} />
{isPending ? (
<p>Loading results...</p>
) : (
<ResultsList results={searchResults} />
)}
</div>
);
}useDeferredValue Hook
useDeferredValue is used to create a low-priority version of a value. This is particularly useful in performance-critical scenarios such as data visualization or user input.
Let's consider a complex list rendering scenario:
import { useState, useDeferredValue } from 'react';
function SearchResults({ query }) {
// We defer the list render to avoid any lag while the user is typing
const deferredQuery = useDeferredValue(query);
// We perform the heavy list rendering process with deferredQuery
// to keep the UI responsive
const results = useMemo(
() => computeExpensiveResults(deferredQuery),
[deferredQuery]
);
return (
<div>
<p>Search results for "{query}":</p>
{query !== deferredQuery && <p>Loading...</p>}
<ul>{results.map(item => <li key={item.id}>{item.name}</li>)}</ul>
</div>
);
}Suspense Improvements
React 18 has significantly improved the Suspense feature. Now, Suspense integrates better with server-side rendering (SSR). This allows for a new feature known as "selective hydration".
In traditional SSR, all page content is rendered server-side as HTML, but all JavaScript must be loaded and run before the content becomes interactive. With React 18, content within a Suspense boundary can be hydrated independently:
// Page component
function HomePage() {
return (
<div>
<Header />
<Suspense fallback={<SkeletonArticle />}>
<ArticleContent />
</Suspense>
<Suspense fallback={<SkeletonSidebar />}>
<Sidebar />
</Suspense>
<Footer />
</div>
);
}
// On the server side
import { renderToPipeableStream } from 'react-dom/server';
app.get('/', (req, res) => {
const { pipe } = renderToPipeableStream(<HomePage />, {
bootstrapScripts: ['/client.js'],
onShellReady() {
res.setHeader('content-type', 'text/html');
pipe(res);
}
});
});With this approach, the page gradually becomes interactive. First, core components (Header, Footer) are hydrated, and then larger components like ArticleContent and Sidebar are hydrated when they are ready. This improves Largest Contentful Paint (LCP) and Time to Interactive (TTI) metrics.
useId Hook
Another important hook introduced in React 18 is useId. This hook is designed to create unique IDs that match on both the client and server sides:
import { useId } from 'react';
function PasswordField() {
const id = useId();
return (
<div>
<label htmlFor={id}>Password:</label>
<input id={id} type="password" />
</div>
);
}This hook is particularly important for accessibility. It is also useful for preventing hydration mismatches during server rendering. Even when multiple instances of the same component are created, a unique ID value is generated for each instance.
Strict Mode Improvements
Strict Mode has been further strengthened in React 18. Now, during development, it simulates mounting, unmounting, and re-mounting your components to detect potential errors in your component's setup and cleanup logic.
This helps identify potential memory leak sources such as database connections, subscriptions, or timers. This feature allows you to catch many problems early, especially in large applications.
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
const root = createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<App />
</StrictMode>
);React 18 Upgrade Strategy
It will be helpful to follow these strategies when upgrading to React 18:
First, switch to the new createRoot API
Update third-party libraries
Test your application and resolve any issues you encounter
Gradually integrate new concurrent features
A typical update might look like this:
// package.json update
{
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}// index.js update
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
// Use createRoot instead of ReactDOM.render
const container = document.getElementById('root');
const root = createRoot(container);
root.render(
<StrictMode>
<App />
</StrictMode>
);Performance Optimization Tips
Technical recommendations to get maximum efficiency from the new features of React 18:
useTransition should be used for heavy computations
useDeferredValue is suitable for large lists and complex data representations
The page loading experience can be made gradual with Suspense
Memoization techniques such as memo, useMemo, and useCallback should be used appropriately
For example, the following pattern can be used in an e-commerce application:
function ProductPage({ productId }) {
const [isPending, startTransition] = useTransition();
// Load main product data immediately
const productData = useProductData(productId);
// Load recommended products with deferred value
const [selectedFilter, setSelectedFilter] = useState('popular');
const deferredFilter = useDeferredValue(selectedFilter);
// Update filter immediately but defer heavy computation
const handleFilterChange = (filter) => {
setSelectedFilter(filter); // Immediate UI update
};
// Compute recommended products (heavy operation)
const recommendedProducts = useMemo(
() => computeRecommendations(productId, deferredFilter),
[productId, deferredFilter]
);
return (
<div>
<ProductDetails data={productData} />
<div>
<h2>Recommended Products</h2>
<FilterButtons
selected={selectedFilter}
onChange={handleFilterChange}
/>
{selectedFilter !== deferredFilter &&
<p>Filtering recommended products...</p>
}
<Suspense fallback={<ProductSkeletons />}>
<RecommendedProductsList products={recommendedProducts} />
</Suspense>
</div>
</div>
);
}React Server Components
One of the most exciting developments with React 18 is React Server Components, which are still in their experimental stage. This feature allows React to work seamlessly on both the server and client sides.
With Server Components, some components can only run on the server and no JavaScript is sent to the client. This reduces bundle size and increases performance. This feature is available in Next.js 13 and later versions.
// server-component.js (runs on server side)
// This file is not sent to the client!
import { db } from '../database';
async function ProductDetails({ id }) {
const product = await db.query('SELECT * FROM products WHERE id = ?', [id]);
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<ClientPrice price={product.price} />
</div>
);
}
// client-component.js (runs on client side)
'use client';
function ClientPrice({ price }) {
const [currency, setCurrency] = useState('USD');
return (
<div>
<select value={currency} onChange={e => setCurrency(e.target.value)}>
<option value="TRY">TRY</option>
<option value="USD">USD</option>
<option value="EUR">EUR</option>
</select>
<p>{formatPrice(price, currency)}</p>
</div>
);
}Adoption of React 18 and Ecosystem
Although React 18 contains significant changes, it is a very strong version in terms of backward compatibility. Most applications can upgrade to React 18 with minimal changes.
It is recommended to implement a gradual transition strategy. It is an effective approach to first upgrade the application to React 18 and then integrate new features as needed. This strategy ensures both a stable application and allows taking advantage of new features.
Conclusion
React 18 provides a significant advancement in the world of JavaScript libraries. With features such as Concurrent Rendering, better server-side rendering, and new hooks, it offers developers new opportunities to provide better performance and user experience.
The changes in this release are not just technical improvements, but paradigm shifts that change the way we think about application architecture and user experience.
The React team has worked meticulously to maintain backward compatibility while developing these features. This allows new features to be adopted gradually and applications to be developed over time.
It's a good time to take a step today to get to know React 18 and start building the web applications of the future.