experimental_taintObjectReference
Block a specific object from being passed unchanged to a Client Component.
experimental_taintObjectReference(message, object);Parameters
| Parameter | Type | Description |
|---|---|---|
$message | string | The error message displayed if the object is passed to a Client Component. It is shown as part of the thrown Error. |
$object | any | The object to taint. Functions, class instances and TypedArray instances are accepted. Functions and class instances are already blocked at the boundary, so tainting them only replaces the default error message. Tainting a TypedArray taints that instance only, not copies of it. |
Return value
undefinedNothing. The call registers the object; any later attempt to pass that same reference to a Client Component throws.
Errors
Throws at render time, with the supplied message, when the tainted object is passed to a Client Component.
Changelog
- PHP experimentalAvailable only in react@experimental; there is no stable release of this API.
- PHP experimentalExported only from the react-server build — importing it in a 'use client' module is not supported.
This marks one object reference as forbidden across the server/client boundary. If that exact object is later handed to a Client Component as a prop, rendering throws with the message you supplied instead of quietly serializing the whole thing.
When to reach for it
Call it at the data-access layer, right where a record is loaded, so every call site inherits the protection. The canonical case is a user or account row: it holds a password hash, an email and a session token alongside the two fields your UI actually needs. Tainting it converts a silent leak into a build-or-render-time error that names the mistake.
What trips people up
Tainting follows the reference, not the data. Spreading the object, cloning it, or picking fields off it produces a new, untainted object — {...user} sails straight through with every secret intact. That is by design, since picking fields is the fix you want, but it means the guard catches only the accident of passing the record whole.
Treat this as one layer, not a security boundary. React's own guidance is explicit: it prevents simple mistakes and does not replace well-designed APIs, isolation, and authorization. It is also experimental and server-only — nothing here exists in the client runtime.
Example
// data/user.js — server-only data access
import { experimental_taintObjectReference } from 'react';
import db from './db';
export async function getUser(id) {
const user = await db.users.get(id); // { id, name, email, passwordHash }
experimental_taintObjectReference(
'Do not pass the entire user object to the client. ' +
'Instead, pick off the specific properties you need for this use case.',
user
);
return user;
}
// Greeting.jsx
// 'use client';
// export default function Greeting({ name }) {
// return <h1>Hello, {name}</h1>;
// }
import Greeting from './Greeting';
// Profile is a Server Component.
export default async function Profile() {
const user = await getUser('u_1');
// Throws with the message above:
// return <Greeting user={user} />;
// Passes: only the field the client actually needs.
return <Greeting name={user.name} />;
}When to use it
- Taint a user record in the data layer so no page can pass it whole to a Client Component.
- Replace the generic "class instances cannot be passed" error with a message naming the fields to pick instead.
- Guard an internal config object that carries API credentials next to display settings.
Frequently asked questions
Why does my tainted object still reach the client?
Almost certainly because something cloned it — a spread, Object.assign, a .map that rebuilds the record, or a JSON round trip all produce a new object that was never tainted. Tainting is tied to the object identity, so you must call it again on any derived object you also want blocked.
Can I use taintObjectReference in a Client Component?
No — it is exported only from the react-server build and is meaningful only while rendering Server Components. Importing it in a 'use client' module will not give you a working function.
Is tainting enough to secure sensitive data?
No, React describes it explicitly as one layer of protection rather than a complete security mechanism. It catches the common accident of forwarding a whole record, but you still need authorization checks, a data layer that never loads secrets it does not need, and APIs that return projections.
More examples
What the taint does and does not catch
Case 1 fails at render with the supplied message. Case 2 renders happily and ships stripeSecret to the browser inside the RSC payload, because the spread produced a different object than the one that was tainted. Case 3 is the pattern the message is asking for. The comparison is the point: the API catches passing the record whole, not leaking its contents by any other route.
import { experimental_taintObjectReference } from 'react';
import ClientPanel from './ClientPanel'; // 'use client'
async function loadAccount() {
const account = {
id: 'acct_1',
label: 'Acme Inc.',
stripeSecret: 'sk_live_…',
};
experimental_taintObjectReference(
'Do not pass the account record to the client; pick id and label.',
account
);
return account;
}
export default async function Billing() {
const account = await loadAccount();
// 1. Throws — the tainted reference itself.
// return <ClientPanel account={account} />;
// 2. Does NOT throw — a spread makes a new, untainted object,
// and stripeSecret is still in it. This is the real hazard.
// return <ClientPanel account={{ ...account }} />;
// 3. Safe — an explicit projection.
return <ClientPanel id={account.id} label={account.label} />;
}Guarding a helper behind a wrapper
Passing the client instance as a prop produces the custom message rather than React's generic complaint about class instances, which tells the next developer what to do instead of only what went wrong. The component itself calls the client on the server and forwards a plain array of id/name pairs, so the token never enters the serialized payload.
// data/secrets.js
import { experimental_taintObjectReference } from 'react';
// A class instance would already be rejected at the boundary, but with an
// unhelpful default message. Tainting replaces it with a useful one.
class ApiClient {
constructor(token) {
this.token = token;
}
async listProjects() {
/* ... */
}
}
let client;
export function getApiClient() {
if (!client) {
client = new ApiClient(process.env.API_TOKEN);
experimental_taintObjectReference(
'The API client holds a live token and must stay on the server. ' +
'Call it in a Server Component and pass the resulting data instead.',
client
);
}
return client;
}
// ProjectList.jsx — Server Component
import ProjectPicker from './ProjectPicker'; // 'use client'
import { getApiClient } from './data/secrets';
export default async function ProjectList() {
const projects = await getApiClient().listProjects();
return (
<ProjectPicker
projects={projects.map((p) => ({ id: p.id, name: p.name }))}
/>
);
}
Discussion