What Is TanStack Redact, a Lightweight React-Compatible Runtime?
TanStack Redact is a lightweight runtime that keeps React's component and Hooks model but renders synchronously, so some APIs behave differently. This article covers setup and design, and compares startTransition in React and Redact.
TanStack Redact is a lightweight runtime that supports React's APIs. It replaces the React implementation used at runtime while letting you keep your existing code written with JSX and Hooks.
Preact is another lightweight alternative to React. Preact itself does not aim to be a reimplementation of React. Instead, it lets you use React code and libraries through a compatibility layer called
preact/compat.
In Projecting React, Tanner Linsley, the author of Redact, recalls that he first tried adopting
preact/compat. According to the post, his application at the time ran into a pile of compatibility issues around the behavior of use(), React 19's Server Actions-related APIs, Portals, Error Boundaries, and the finer details of hydration, and the extra compatibility code kept growing.
So Redact took a different approach: start from React's public API and build an implementation scoped to what TanStack Start needs. Whereas Preact stacks a compatibility layer on top of itself, Redact builds its implementation from the React APIs and behaviors that are actually needed.
That said, sharing the same API names does not mean that all of React's behavior is reproduced. Redact uses synchronous rendering and omits the mechanism that interrupts and resumes rendering based on priority. This difference affects how useDeferredValue and startTransition behave.
In this article, I'll explain what TanStack Redact does and how it differs from React's design philosophy.
Getting Started with TanStack Redact
In a typical React application, you import things like useState from react and createRoot from react-dom/client. Redact uses your build tool to swap these import targets for its own implementation.
For example, the imports you write in your application code are exactly the same as with regular React:
import { startTransition, useState } from "react";
import { createRoot } from "react-dom/client";With Redact's Vite plugin, react resolves to @tanstack/redact, and react-dom/client resolves to @tanstack/redact/dom-client. Modules used to run JSX, such as react/jsx-runtime, are replaced in the same way.
Adding Redact to Vite
Let's use Redact in a Vite + React project. First, install the @tanstack/redact package.
npm install --save-exact @tanstack/[email protected]Next, add redact() to your Vite plugins. If you use Vite's built-in JSX transform, you also need to specify esbuild: { jsx: 'automatic' } explicitly. If you use the React Vite plugin, the automatic JSX runtime is enabled by default, so you don't need to specify it.
import { defineConfig } from 'vite';
import { redact } from '@tanstack/redact/vite';
export default defineConfig({
plugins: [redact()],
esbuild: { jsx: 'automatic' },
});Redact's plugin handles client and SSR builds. Note, however, that it does not swap imports in the React Server Components (RSC) environment, where the original React is used as is.
How Redact Differs from React's Design Philosophy
To understand Redact's design, let's first look at what React prioritizes. React places a few constraints on components so that it can control when rendering happens.
One of React's core ideas is keeping components pure. A component should return the same result for the same props, state, and context, and it should not modify external state during rendering. Side effects run outside of rendering, in places like event handlers and Effects.
These constraints make it easier for React to control when components are evaluated. React's official documentation also explains how purity relates to prioritizing rendering.
Prioritizing Input over Heavy UI Updates
One reason to control when rendering happens is to keep the UI responsive to user interactions. For example, imagine a screen that updates a huge list as the user types into a search field. If updating the input field has to wait until the list finishes rendering, typing feels sluggish.
React introduced concurrent rendering, a mechanism that controls rendering priority so that updates to the input field come first. With concurrent rendering, React can pause rendering midway and handle higher-priority updates first. The React 18 announcement explains how this differs from the previous, uninterruptible rendering.
Developers use the following APIs to tell React how to treat updates:
startTransition: Treats the state updates inside the callback as a Transition that doesn't block other updatesuseDeferredValue: Defers reflecting a value and attempts to update the UI that uses it later
Let's look at an example. We separate text, the value of the input field, from query, the value used to filter the list. By applying the input field update as a regular state update and wrapping only the list update in startTransition, we can prioritize updates to the input field.
To make the behavior easier to observe, the ProductList component that renders the list intentionally runs a heavy calculation for each of its 5,000 product rows before deciding whether to display the row. Because it is wrapped in memo, updates that only affect the input field, where the query passed to the list hasn't changed, can skip re-rendering the list.
import { startTransition, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { ProductList } from './ProductList.jsx';
function App() {
const [text, setText] = useState('');
const [query, setQuery] = useState('');
return (
<main>
<label>
商品を検索
<input
value={text}
onChange={(event) => {
const nextText = event.target.value;
setText(nextText); // Don't make the input field update a Transition
startTransition(() => {
setQuery(nextText); // Make the list update a Transition
});
}}
/>
</label>
<p>{text !== query ? '一覧を更新しています' : ''}</p>
{/* The ProductList component intentionally includes a heavy calculation */}
<ProductList query={query} />
</main>
);
}Because the list update triggered by setQuery(nextText) is treated as a Transition, if another keystroke arrives while the list is rendering, React can interrupt the list rendering and prioritize the input field update. It then re-renders the list with the latest search term. In the demo below, you can see that typing is not blocked.
Redact's Choice: Synchronous Rendering
Redact aims to provide React's APIs and everyday behavior without concurrent scheduling. Here, scheduling means managing the priority and timing of rendering work. The size analysis for the initial 0.0.1 release describes a strategy of keeping the implementation small by omitting features such as priority-based interruptible rendering.
Later, PR #24, merged on September 11, 2026, expanded compatibility with React's APIs, the DOM, and SSR. Even with this change, it explicitly keeps synchronous rendering and continues to omit the behavior of Hooks related to Actions and optimistic updates.
In other words, the design inherits React's component model while narrowing the runtime's responsibilities. Like React, it assumes pure components, but it reduces the work the runtime takes on.
The Impact of Omitting Interrupt-and-Resume Support
In the earlier search example, React needs its own machinery to interrupt list rendering and prioritize input. It has to manage update priorities, remember how far rendering has progressed, and decide when to yield. When new input arrives, it also needs to restart the in-progress rendering.
React's rendering implementation includes
workInProgress, which tracks the component currently being worked on; lanes, which classify things like update priorities; and shouldYield, which decides whether to yield. The JavaScript that manages and updates these is also shipped as part of the runtime.
Redact reduces bundle size by omitting or simplifying the following work:
| Work | What interruptible rendering requires | Redact's approach |
|---|---|---|
| Choosing updates | Distinguish input updates from Transition updates and choose which work to do first | Does not implement React's lane-based priorities |
| Rendering progress | Split work, decide whether to yield, and resume from where it left off | Does not implement yielding in the middle of rendering |
| Deferring values | Manage showing the old value while attempting a render with the new value | useDeferredValue returns the given value as is |
The difference in useDeferredValue is obvious when you look at Redact's Hooks implementation.
useDeferredValue<T>(v: T): T {
return v
}This method returns the value it receives as is. There is none of React's logic for holding on to the previous value and rendering at a different priority.
Other Ways Redact Stays Small
Beyond synchronous rendering, Redact also simplifies event handling. Its DOM event handling registers handlers on elements with
addEventListener and adds compatibility properties such as nativeEvent to native events, rather than reproducing React's entire synthetic event system.
In addition, when you disable a feature flag, the Vite plugin swaps that feature's import target for a simplified implementation. Instead of shipping the real implementation to the browser and turning it off at runtime, the implementation is excluded at build time. The
nano preset uses this mechanism to disable optional features all at once, letting you opt in to the ones you need.
import { redact } from "@tanstack/redact/vite";
export default defineConfig({
plugins: [
redact({
preset: "nano",
features: { context: true },
}),
],
});The feature flag documentation states, for example, that disabling
context means Provider values are not passed down, and disabling hydration makes hydrateRoot throw an error.
Comparing the Same Product List in React and Redact
Let's run the same product list with Redact. The code stays the same; simply enabling the Vite plugin replaces the React imports with Redact's implementation.
In Redact, even if you call startTransition, the input field update and the list update are treated with the same priority, just as if you hadn't used it. In the demo below, you can see that typing into the input field lags.
On the other hand, Redact (with the full preset) produces a smaller build. When I built this product list for production with Vite, the gzip size of all generated JavaScript was 69,714 bytes with React and 20,112 bytes with Redact.
Summary
- TanStack Redact is a lightweight runtime that swaps React's import targets at build time. It keeps the basic way of writing code with components and Hooks and supports React's APIs, but it does not reproduce all of React's behavior.
- Redact uses synchronous rendering and omits concurrent scheduling.
useDeferredValueandstartTransitionexist as APIs, but they do not provide the same priority control as React. - Redact's
nanopreset disables optional features all at once and can reduce bundle size even further.




