When a React component receives data from elsewhere in your application - or from an external source - there’s always a chance that the shape of that data isn’t what you expect. In plain JavaScript these errors can surface only at runtime, often deep inside the component tree. A silent type mismatch can lead to hard‑to‑debug bugs, broken UI, or even security vulnerabilities.
Prop type validation sits at the boundary where data enters your component. By validating props at runtime you guarantee that the component behaves predictably, no matter where its data originates.
Prior to React 19, the prop-types package was the go‑to tool for validating component props. When a prop type mismatch occurred, it would emit a warning in the browser console. However, React 19 removed the support of this package, so you’d have to re‑introduce it manually. Fortunately, there are more robust options available and one popular choice is Zod.
What is Zod?
Zod is a lightweight, declarative schema language. It lets you describe the shape of an object - its required keys, optional keys, value types, and custom constraints - and then run that description against actual data at runtime. If the data fails to match, Zod throws an informative error that pinpoints exactly which field is wrong and why.
A Zod schema is a description of the shape of a value. Think of it like a blueprint: it says “this should be a string, that should be a number, that optional field should be a boolean, etc.”. The key benefit is that the same schema can be used for both compile‑time type inference and runtime validation.
const UserSchema = z.object({
id: z.number().int().positive(),
username: z.string().min(3),
email: z.string().email(),
isActive: z.boolean().optional()
});
Prop type validation in React
To automatically validate prop types at runtime with Zod, we must:
- Extend React’s rendering logic so that Zod schemas are checked during the JSX compilation step automatically.
- Create a Zod schema for a component and attach it directly to the component.
Extend React
In modern React, JSX is transformed into JavaScript at runtime using two core helpers: jsxDEV from react/jsx-dev-runtime (in development) and jsx from react/jsx-runtime (in production). To enable automatic prop type validation, we need to overwrite these helpers so that Zod is automatically called when the component is rendered.
// customReactJSX.js
import * as jsxDevRuntime from 'react/jsx-dev-runtime';
import * as jsxRuntime from 'react/jsx-runtime';
import { z } from 'zod';
const { jsxDEV } = jsxDevRuntime;
const { jsx } = jsxRuntime;
function propValidation(props, schema, componentName) {
try {
// Validate the schema
schema.parse(props);
} catch(e) {
// If there is a schema validation error, we extract relevant information
// from the ZodError and show which validation(s) failed in the console.
if (e instanceof z.ZodError) {
e.issues.forEach(issue => {
const prop = issue.path[0];
if (prop) {
// Try to find which component triggered the error
const regexp = /(at )?propValidation(?!\/)[^.]/;
let originatedAt;
const error = new Error();
if (error?.stack) {
const stackLines = error.stack.split('\n');
for (let i = 0, ii = stackLines.length; i < ii; i++) {
const line = stackLines[i];
if (regexp.test(line) === true) {
originatedAt = stackLines[i + 2]?.trim();
if (originatedAt.startsWith('at')) {
originatedAt = originatedAt.slice(3).trim();
}
break;
}
}
}
console.warn(`Prop type validation failed: ${issue.message} for prop "${prop}" supplied to "${componentName}" ${originatedAt ? 'at ' + originatedAt : ''}`, {
originalError: issue
});
}
});
} else {
console.warn("Was not able to validate prop schema");
}
}
}
// Development mode
const customJSXDev = (component, props, ...children) => {
if (component && component.propSchema) {
propValidation(props, component.propSchema, component.name);
}
return jsxDEV(component, props, ...children);
};
// Production mode
const customJSX = (component, props, ...children) => {
if (component && component.propSchema) {
propValidation(props, component.propSchema, component.name);
}
return jsxDEV(component, props, ...children);
};
jsxDevRuntime.jsxDEV = customJSXDev;
jsxRuntime.jsx = customJSX;
To activate these modified helpers, we must import customReactJSX.js once at the start of our application - for example:
// index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './customReactJSX';
Schema definition
Now, in order to validate component props with the modified JSX helpers, we have to create a Zod schema for a component and bind it to the component itself. This mirrors the way you would normally use the prop-types package, but we store the schema in a custom property called propSchema so as not to clash with the existing PropTypes API.
// MyComponent.jsx
import { z } from 'zod';
function MyComponent(props) {
return <>...</>;
}
MyComponent.propSchema = z.object({
name: z.string()
});
With these changes in place, props are now automatically validated for each component. Here is an example of <MyComponent /> being used without a value for the name prop being provided:
Final words
Zod makes runtime prop type validation feel almost like a built‑in feature and it gives you the best of both worlds: the type safety and inference benefits of a strong schema language, together with the instant feedback of a traditional prop type checker. Using the approach described in this tutorial, you get automatic validation across the entire component tree without any additional boilerplate when you write a component.
Of course, there are other schema validation libraries out there, and if you prefer AJV, JSON Schema, or any other library, you can integrate them in the same way: attach the schema to your component and hook it into the custom JSX helpers.

Top comments (0)