This content originally appeared on Bits and Pieces - Medium and was authored by Prithwish Nath
Here’s a ‘How to React.js’ for Version 18. With a bunch of powerful new tools at your disposal, here’s how you navigate the new layers of abstraction to build what you need to build.
React 18 (18.2.0, at the time of writing), though, takes giant steps towards bridging this gap, with many new features out-of-the-box such as Concurrent Rendering, Transitions, and Suspense; with a bunch of Quality-of-life changes sprinkled on top.
The tradeoff? More “magical” abstractions. Not everyone is going to be a fan of this approach, but in terms of end results, let’s just say that now it would actually be viable for me to consider skipping a batteries-included framework for my next project, build it in React 18 instead, with something likereact-query as my data fetching/caching solution.
Confession: I don’t use React much these days. Vanilla React, that is. Next.js (I’ve covered the intricacies of Release 13 here!), or a Astro + Preact combo are the tools I reach for, instead. Don’t get me wrong — React is still great. Often, though, you might feel like its viability is very much dependent on how much time you’re willing to invest in learning its quirks, and how much code you’re willing to commit to optimization hacks.
What convinced me? Let’s take a look.
1. Concurrent Rendering
Pop quiz time. Is JavaScript single-threaded?
JavaScript itself is single-threaded (with initial code execution taking place ASAP, not waiting for the DOM tree to be completed), but anything based on browser Web APIs (AJAX requests, rendering, event triggers etc.) is not. This divide has often been apparent to you, as a React developer, while data fetching from different components independently and running into race conditions.
To bridge this gap, then, we look to concurrency. This gives React parallelism, and the potential to match native device UI in terms of responsiveness.
How? To answer that, let’s have a look at how React works under the hood.
Core to React’s design is maintaining a virtual or shadow DOM; a copy of the rendered DOM tree, with each individual node representing a React element. After any UI update, React recursively diffs the two trees, and commits the cumulative changes to a render pass.
React 16 introduced a new algorithm for doing this — it was called React Fiber, and it replaced a stack-based algorithm. Every single React element/component/whatever-you-want-to-call-it, is a Fiber, and each Fiber’s children/siblings can be lazy-rendered. React achieved orders of magnitude better UI responsiveness by lazy-rendering each of these Fibers. Here’s a visual comparison.
React 17 built on this, and React 18 gives you even more control over this process.
What React 18 does is hold off on that post-DOM tree diffing render pass until the entire subtree has been evaluated. The end result? Every rendering pass is now interruptible. React can selectively update parts of the UI in the background, or pause/resume/abandon an in-process render, all the while guaranteeing the UI doesn’t break, drop frames, or have inconsistent frametimes (i.e. a 60 FPS UI should always take 16.67 milliseconds to render each frame).
💡 This is going to be an absolute game changer for mobile devices when React 18 comes to React Native.
Concurrent Rendering is the core concept behind the React 18 features I’m going to cover — Suspense, Streaming HTML, the Transition API, and more. Every time you use any of these new features, you’re opting into Concurrent React. You won’t have to explicitly have to think about what goes on behind the scenes.
2. Suspense
Suspense has existed since React 16.6.0, but it could only be used for dynamic imports with React.lazy like so :
const CustomButton = React.lazy(() => import(‘./components/CustomButton’));
With React 18, Suspense has been expanded, and made more general in its application. Do you have a component tree that isn’t done fetching data yet so it can’t really show anything? Specify a default, non-blocking loading state your users should see, until you can show data.
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
You improve UX this way because:
- Your users don’t have to wait for all the data to be fetched before they can see anything at all.
- Your users see a loading spinner, flashing skeleton, or just a <p> Loading…</p> that provides them immediate feedback that something, anything, is happening; the app didn’t crash.
- Your users don’t have to wait for all interactive elements/components in your page to finish hydrating before they can interact with anything at all. <Comments> hasn’t loaded yet? That’s fine, nothing blocks them from clicking through and browsing data in <LatestArticles>, <Navbar>, or <Post> anyway.
You also improve DX, because now, React defines a canonical “loading state” you should think of when building apps with React (or a meta-framework like Next.js or Hydrogen). Plus, if you know how to do try-catch blocks in vanilla JS, you already know how to use Suspense boundaries:
- Instead of errors, <Suspense> catches components that are “suspending” i.e. saying “Hey, I’m not ready to show anything yet!” because of missing data, code, whatever.
- Just how when you throw an error, it triggers the nearest catch block, the closest <Suspense> will catch a suspending component below it and show its fallback UI, no matter how many components are in between.
Suspense boundaries let you design your UI in a more fine-grained manner, with an official concept of a loading state in the React programming model. But you can make it even more robust when combining it with the Transition API to specify render ‘priorities’ of components, making this the perfect segue into…
3. Transition API
Have I talked about my favorite React custom hook?
It’s a pretty simple one that has served me well through at least a dozen products shipped. I’d consider it invaluable for any <SearchField> / user input component I write.
The idea behind it is pretty simple; if you have a search bar where users can type in queries or filters, you don’t want to be making updates to your visible list (or worse, search API calls!) on every single key stroke. This hook throttles — ‘debounces’ — these calls, making sure you don’t thrash your servers.
But the obvious drawback is that this increases perceived lag. You’re essentially introducing arbitrary slowness, and trading UI responsiveness in favor of making sure your app’s internals don’t blow up.
In React 18, Concurrency enables a better — and intuitive — approach: freely interrupt these calculations (and their renders) when we receive a new state, giving you responsiveness as well as stability.
The new Transitions API allows you to fine-tune this further: splitting state updates into ones that are Urgent (in this SearchField example this would be your typing, clicking, highlighting, updating the query text) and ones that can take a backseat until data is ready — Transition updates. (updating the actual list). Transitions are freely interruptible, don’t block user input, and keep your app more responsive.
import { startTransition } from 'react';
// UI updates are Urgent
setSearchFieldValue(input);
// State updates are Transitions
startTransition(() => {
setSearchQuery(input);
});
As you might have guessed, this works even better with Suspense boundaries, especially to avoid one very blatant UI issue: if you suspend during a transition, React will actually just use old state and show old data, instead of replacing already-visible content with the fallback. The new render will be delayed until enough data has loaded, instead.
Suspense, Transitions, Streaming SSR — see how much Concurrent React improves both UX and DX?
4. Server Components
The other most significant new feature in React 18, and one that makes building things for the web far, far easier. The only catch being…they’re still not stable enough for general use, and can only be used via meta-frameworks like Next.js 13.
React Server Components (RSCs) are literally just components that render only on the server, not the client.
What are the implications of this? Plenty. But here’s the TL;DR :
- You ship literally zero JavaScript to the client when using RSCs. This alone is a massive gain as you no longer have to worry about shipping heavy client-side libraries (GraphQL clients is a common example) that inflate your production bundle sizes and Time-To-First-Byte.
- You can directly run data fetching operations — database queries, APIs, microservice interactions — inside them, and then simply pass the resulting data to client components (i.e. traditional React components) via props. These queries will also always be orders-of-magnitude faster because your server is typically much, much faster than your client, and only use client-server communication for UI, NOT data.
- RSCs go hand-in-glove with Suspense, as you can fetch data on the server and incrementally streaming rendered units of UI to the client. RSCs also do not lose client-side state on a reload/refetch, ensuring a consistent UI/UX.
- You cannot use hooks like useState/useEffect, event listeners like onClick(), browser APIs that access the canvas or clipboard, or CSS-in-JS engines like emotion or styled-components.
- You can share code between the server and client, making it much easier to ensure type-safety.
Webdev just got a whole lot easier now that you can mix-and-match server and client components, selectively flipping between the two depending on if you need performance at small bundle sizes, or rich user interactivity. This lets you build flexible, versatile hybrid apps that are resilient to changing tech/business requirements.
5. Automatic Batching — Performance Optimizations behind the scenes
You already know how React renders stuff under the hood: one state update = one new render.
What you might not have known is how, as an optimization, React also batches multiple state updates together into one render pass (because, of course, state updates = re-renders, and you’d want to minimize that).
In React <=17 this only happened inside event listeners. Anything outside of a React-managed event handler was not batched, and that included anything in a Promise.then(), after an await, or in a setTimeout. As a result, you might have experienced unintended, multiple re-renders because that behind-the-scenes batching was based on call stacks, and Promises or callbacks = multiple new call stacks outside of the first browser event = multiple batches = multiple render passes.
What’s changed, then? Well, React is now intelligent enough to group together all state updates queued together into one event loop, making sure you get as few re-renders as possible. This isn’t something you have to think about or opt-into; it happens automatically behind the scenes in React 18.
function App() {
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(true);
function handleClick() {
fetch('/api').then((response) => {
setData(response.json()); // In React 17 this causes re-render #1
setIsLoading(false); // In React 17 this causes re-render #2
// In React 18 the first and only re-render is triggered here, AFTER the 2 state updates
});
}
return (
<div>
<button onClick={handleClick}> Get Data </button>
<div> {JSON.stringify(data)} </div>
</div>
);
}
6. Native Support For Async/Await — Introducing the use hook.
Great news! React has finally embraced the fact that most data operations are asynchronous, and added native support for the same in React 18. So what does this look like in terms of DX? There’s two parts to this:
- Server components can’t use hooks, but they don’t need to. They’re stateless and can simply async/await any Promise now.
- Client components, though, are not async, and can’t use await to unwrap the value from a Promise. React 18 provides a brand new use hook for them instead.
The use hook (not a fan of this naming, BTW) is the only React hook that can be called conditionally, and from anywhere at all — even loops. In the future, React will include support for unwrapping other values too, including Context.
How do you use… use?
import { experimental_use as use, Suspense } from 'react';
const getData = fetch("/api/posts").then((res) => res.json());
const Posts = () => {
const data = use(getData);
return <div> { JSON.stringify(data) } </div>
};
function App() {
return (
<div>
<Suspense fallback={ <Spinner /> }>
<Posts />
</Suspense>
</div>
);
}
Simple, yes, but also incredibly easy to shoot your foot off with. For example, you might find yourself in this scenario:
import { experimental_use as use, Suspense } from 'react';
// Whoops. You just triggered infinite loading.
const PostsWhoops = () => {
// Because this will ultimately always resolve to a new reference.
const data = use(fetch("/api/posts").then(res) => res.json()));
return <div> { JSON.stringify(data) } </div>
};
// The right approach.
const getData = fetch("/api/posts").then((res) => res.json());
const Posts = () => {
const data = use(getData);
return <div> { JSON.stringify(data) } </div>
};
// ...
}
Why does this happen?
Consider what might happen if the use hook unwrapped a Promise that hadn’t yet finished loading (whatever the bottleneck may have been; network speed, or bad data). In this case (i.e. used in a Suspense boundary) use will suspend, but since a component does not work the same way as a vanilla JS async/await, it will not resume execution at the point of failure, but instead do-over the component’s render entirely when the promise finally resolves, and return the actual — non-undefined — value of the unwrapped promise on the next render attempt.
Unfortunately, this would mean the reference to the Promise would now be a new one entirely every single time, the process would repeat back on itself, and this is why you’d get the infinite render loop here.
To avoid this, you’re meant to use use together with the upcoming Cache API to add a caching strategy to all fetch calls. Next.js 13 implements this as its new, extended fetch API.
But yeah, that’s the lowdown. React now has full native support for async code on both the server and the client, ensuring full compatibility with the rest of JavaScript land.
How do I Update?
You’re probably using it right now! CRA, Vite, and Next.js starter templates via npx all use React 18.2.0.
If upgrading an existing app from React <=17 though, you’ll need to keep a few things in mind.
1. Moving to createRoot.
There’s a new API for managing roots, and ReactDOM.render is no longer supported. Use createRoot instead (this also opts you into the new Concurrent renderer, enabling all the shiny new features). Until you do this, though, your app won’t break — but will behave as if it’s running React 17, and you’d get none of the benefits.
// React 17
import { render } from 'react-dom';
const container = document.getElementById('app');
render(<App tab="home" />, container);
// React 18
import { createRoot } from 'react-dom/client';
const container = document.getElementById('app');
const root = createRoot(container); // createRoot(container!) if you use TypeScript
root.render(<App tab="home" />);
2. Moving to hydrateRoot.
Similarly, for SSR, ReactDOM.hydrate is no longer a thing. Use hydrateRoot instead. Again, if you don’t, React 18 behaves like React 17 instead.
// React 17
import { hydrate } from 'react-dom';
const container = document.getElementById('app');
hydrate(<App tab="home" />, container);
// React 18
import { hydrateRoot } from 'react-dom/client';
const container = document.getElementById('app');
const root = hydrateRoot(container, <App tab="home" />);
3. Render callbacks are now gone.
If your app used a callback as the third argument for the render function, you’ll have to wrap that in a useEffect instead if you want to keep it. The old way breaks Suspense.
// React 17
const container = document.getElementById('app');
render(<App tab="home" />, container, () => {
console.log('rendered');
});
// React 18
function AppWithCallbackAfterRender() {
useEffect(() => {
console.log('rendered');
});
return <App tab="home" />
}
const container = document.getElementById('app');
const root = createRoot(container);
root.render(<AppWithCallbackAfterRender />);
4. Strict Mode.
Concurrency is a huge performance boost for React, but it expects your components to be compatible with reusable state (remember, for concurrency we’ll need to be able to interrupt renders-in-progress while reusing old state to ensure consistent UI).
To flush out some of the anti-patterns, React 18’s Strict Mode will simulate effects being mounted and destroyed multiple times (by calling Functional Components, Initializers, and Updaters twice) like so:
Step 1 : Component is mounted (Layout effect code runs + Effect effect code runs)
Step 2 : React simulates components being hidden/unmounted (Layout effect cleanup code runs + Effect effect cleanup code runs)
Step 3 : React simulates components being remounted with old state (Step 1 all over again)
This is to surface concurrency-related bugs and mistakes regarding React’s philosophy of keeping components pure. Here’s an example:
setTodos(prevTodos => {
prevTodos.push(createTodo());
});
This is an impure function because you’re directly mutating state — modifying this array. In Strict Mode, React calls Updater functions twice, meaning you’ll see the same Todo added twice, and know immediately what your error was.
To fix this, do it the right way: replace the array instead of mutating state in-place:
setTodos(prevTodos => {
return […prevTodos, createTodo()];
});
If your components, initializers, and updaters were already idempotent, this purposeful double-render won’t break your code as it’s only in dev mode, and doesn’t affect production behavior. Event handlers aren’t affected by the new Strict Mode behavior as they don’t need to be pure functions.
5. For TypeScript.
If you’re using TypeScript (as you should), you’ll need to also update your type definitions (@types/react and @types/react-dom) to the latest versions. Also, listing the children prop explicitly is a requirement now.
interface MyButtonProps {
color: string;
children?: React.ReactNode;
}
6. Internet Explorer is no longer supported.
The code for it is still there, though, and they probably won’t remove it until React 19, but you should stay on React 17 if you absolutely must support IE.
The Days Ahead
React 18 is a fantastic step in the right direction that bodes well for the future of everybody’s favorite webdev ecosystem. But if you didn’t like React’s quirks and abstractions, this release won’t win you over — there are plenty of awesome new features, but even more magical abstractions.
That being said, here’s what the workflow now looks like for you as a React developer using 18.2.0:
- By default, render components on the server (data operations, auth, anything you’d want to have as backend code)
- Opt-in to client components when you need interactivity (useState/useEffect/DOM APIs)
- Stream the results.
Faster page loads, smaller JavaScript bundles, shorter time-to-interactive, all ensuring better UX and DX. What’s next for React? Based on this, my guess is an auto-memoizing compiler. Exciting times ahead!
What’s New in React.js v18: New Toys, New Footguns, & New Possibilities. was originally published in Bits and Pieces on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Bits and Pieces - Medium and was authored by Prithwish Nath
Prithwish Nath | Sciencx (2022-11-15T07:02:46+00:00) What’s New in React.js v18: New Toys, New Footguns, & New Possibilities.. Retrieved from https://www.scien.cx/2022/11/15/whats-new-in-react-js-v18-new-toys-new-footguns-new-possibilities/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.