New React features that are going to make it easier to build web appsβ¦ And more chatbots π
<Activity />: Controlled rendering of βactivitiesβThe <Activity /> component letβs you group parts of the UI and decide whether they should be active or hidden.
This feature is useful for handling animations and loading states, rendering data and various elements within a layout.
visiblehiddendisplay: "none" CSS propertyimport { useState, Activity } from "react";
export default function Page() {
const [showFilters, setShowFilters] = useState(false);
return (
<>
<button onClick={() => setShowFilters((v) => !v)}>
{showFilters ? "Hide" : "Show"} filters
</button>
<Activity mode={showFilters ? "visible" : "hidden"}>
<FiltersPanel />
</Activity>
</>
);
}
import { Activity, startTransition, use } from "react";
import { fetchProduct } from "./data";
export default function ProductShell({ nextId }) {
function warmNext() {
startTransition(() => {
use(fetchProduct(nextId)); // data loads in background
});
}
return (
<>
<button onClick={warmNext}>Preload next</button>
<Activity mode="hidden">
<ProductPage id={nextId} />
</Activity>
</>
);
}
<Activity mode={open ? "visible" : "hidden"}>
<aside className="w-72 shrink-0">
<Sidebar />
</aside>
</Activity>
<main className="min-w-0">
<Content />
</main>
useEffectEvent: Fixing stale closures in effectsAn effect usually depends on props and this means having to restart it every time your props change. We can now define an event-like callback, that always sees the latest props/state but doesnβt need to be in the effectβs dependency list (which is great, as useEffect was taking itβs toll).
import { useEffect, useEffectEvent } from "react";
import { socket } from "./socket";
export default function Chat({ theme }) {
const onMessage = useEffectEvent((msg) => {
notifyUser(msg, { theme }); // always latest theme
});
useEffect(() => {
function handler(msg) {
onMessage(msg);
}
socket.on("message", handler);
return () => socket.off("message", handler);
}, []); // no need to list theme
}
import { useEffect, useEffectEvent, useState } from "react";
export default function Clock({ format }) {
const [now, setNow] = useState(() => new Date());
const tick = useEffectEvent(() => {
setNow(new Date());
logHeartbeat(format); // stays fresh
});
useEffect(() => {
const id = setInterval(() => tick(), 1000);
return () => clearInterval(id);
}, []);
return <time>{formatTime(now, format)}</time>;
}
import { useEffect, useEffectEvent, useState } from "react";
export function Search({ query }) {
const [results, setResults] = useState([]);
const onResolve = useEffectEvent((data) => setResults(data));
useEffect(() => {
let cancelled = false;
fetch(`/api/search?q=${encodeURIComponent(query)}`)
.then((a) => a.json())
.then((b) => {
if (!cancelled) onResolve(b);
});
return () => {
cancelled = true;
};
}, [query]);
return <Results items={results} />;
}
cacheSignal: Abort work when rendering endsFor server components, this version of react ensures we can abort cached operations automatically when rendering finishes, fails or is cancelled.
import { cache, cacheSignal } from "react";
const getProduct = cache(async (id) => {
const res = await fetch(`https://api.example.com/products/${id}`, {
signal: cacheSignal(),
cache: "no-store",
});
return res.json();
});
export default async function ProductPage({ id }: { id }) {
const product = await getProduct(id);
return <ProductView product={product} />;
}
import { cache, cacheSignal } from "react";
const expensive = cache(async () => {
const ctrl = new AbortController();
cacheSignal().addEventListener("abort", () => ctrl.abort());
return fetch("https://api.example.com/big", { signal: ctrl.signal }).then(
(r) => r.json()
);
});
Render part of your app now, ship it and resume later.
import { prerender, resume } from "react-dom/server.edge";
import App from "./App";
const { prelude, postponed } = await prerender(<App />);
await sendHtml(prelude); // send static shell fast
const stream = await resume(postponed);
return new Response(stream, { headers: { "content-type": "text/html" } });
import { prerender, resumeToPipeableStream } from "react-dom/server";
import App from "./App";
const { prelude, postponed } = await prerender(<App />);
res.setHeader("Content-Type", "text/html");
res.write(prelude);
const stream = await resumeToPipeableStream(postponed, {
onAllReady() {
stream.pipe(res);
},
});
import { Suspense } from "react";
export default function App() {
return (
<>
<Hero />
<Suspense fallback={<Skeleton />}>
<Products />
<Reviews />
</Suspense>
</>
);
}
eslint-plugin-react-hooks@6.1.0: flat config is now recommended// eslint.config.js
import reactHooks from "eslint-plugin-react-hooks";
export default [reactHooks.configs["recommended"]];
useId prefix change: IDs now look like _s_510, valid for XML and view-transition APIsconst id = useId();
<label htmlFor={id}>Email</label>
<input id={id} />