Difficulty: Beginner
What are props in React? How do you validate them, and what are common patterns for passing data?
Props are the lifeblood of React's component model. If you understand props deeply - not just how to use them, but the philosophy behind them - you understand a huge chunk of how React thinks about building UIs.
At the simplest level, props are how you pass data from a parent component to a child component. The word "props" is short for "properties," and you can think of them like function arguments. When you write <UserCard name="Alice" age={28} />, you're passing two props to UserCard: a string name and a number age. Inside UserCard, you receive these as an object: function UserCard({ name, age }) { ... }.
But here's the critical rule that makes React's architecture work: props are read-only. A child component must never modify the props it receives. This isn't just a convention - it's a fundamental design principle. React enforces a unidirectional data flow: data flows down from parent to child via props, and events (like user actions) flow up from child to parent via callback functions passed as props. This one-way flow makes your application predictable. When something goes wrong, you can trace the data from its source (the parent that owns the state) down to wherever it's being displayed. There's no mystery about where data comes from.
Props can be literally any JavaScript value. Strings, numbers, booleans, objects, arrays, functions, even other React elements. This flexibility is part of what makes React's component model so powerful. You can pass a click handler as a prop (onClick={() => doSomething()}), an entire component as a prop (icon={<SearchIcon />}), or a render function as a prop for more advanced patterns.
The children prop deserves special attention because it's both a regular prop and a special one. Whatever you put between the opening and closing tags of a component becomes the children prop. So <Card><h1>Title</h1></Card> passes <h1>Title</h1> as props.children to Card. This is the foundation of the composition pattern in React - building complex UIs by nesting simple components inside each other, like assembling Lego bricks. You can write a Card component that just adds a border and shadow around whatever children you give it, and use it to wrap anything. This is far more flexible than inheritance.
You can even pass a function as children - this is called the "render props" pattern. The parent component calls the children function with some data, and the child decides how to render it. This was one of the main patterns for sharing stateful logic before hooks came along, and it's still useful in certain scenarios.
For type-checking props, the evolution has been interesting. React originally had PropTypes - a runtime validation system that would log warnings in the browser console during development if props didn't match the expected types. PropTypes was extracted into its own package (prop-types) and still works, but it only catches errors at runtime, and only in development mode. It provides zero help in your editor while you're writing code.
In modern React projects, TypeScript has almost entirely replaced PropTypes. With TypeScript, you define your prop types using interfaces or type aliases, and you get compile-time checking, autocomplete, and inline documentation right in your editor. If you try to pass a number where a string is expected, your code won't even compile. This is orders of magnitude better than a console warning that you might miss. If you're starting a new project today, use TypeScript for props and skip PropTypes entirely.
For default prop values, the old approach was the static defaultProps property on the component. In modern function components, this is deprecated - just use JavaScript default parameters directly in the function signature: function Button({ variant = 'primary', size = 'md' }) { ... }. Clean, simple, and works exactly like default parameters in any JavaScript function.
There are a few important patterns worth knowing. Destructuring props in the function signature is standard practice - it makes it immediately clear what props a component accepts. The rest/spread pattern (function Input({ label, error, ...rest }) where you spread ...rest onto the underlying <input>) is invaluable for wrapper components. It lets your custom Input component accept every prop that a native HTML input accepts, without you having to list them all.
But be careful with prop spreading. If you spread arbitrary props onto a DOM element, you might pass non-standard HTML attributes, which will generate console warnings and won't do anything useful. Always destructure out the props your component handles, and only spread the remaining props onto the DOM element if you're confident they're valid HTML attributes.
One more thing about data flow: when a child component needs to change something, the parent should pass a callback function as a prop. The child calls the function (onDelete(item.id)), the parent handles the logic and updates its own state, and React re-renders the child with updated props. This is the "events flow up" part of React's unidirectional data flow, and it's essential to understand.
// Define prop types with TypeScript interface
interface ButtonProps {
variant: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg'; // optional with ?
disabled?: boolean;
onClick: () => void;
children: React.ReactNode;
}
// Destructure with defaults
function Button({
variant,
size = 'md',
disabled = false,
onClick,
children
}: ButtonProps) {
return (
<button
className={`btn btn-${variant} btn-${size}`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
}
// Usage
<Button variant="primary" onClick={() => alert('clicked')}>
Click Me
</Button>
TypeScript interfaces provide compile-time prop validation. Default parameter values replace the deprecated defaultProps for function components.
// Children as content
function Card({ title, children }) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-body">{children}</div>
</div>
);
}
// Render props pattern - children as function
function DataFetcher({ url, children }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(data => { setData(data); setLoading(false); });
}, [url]);
return children({ data, loading });
}
// Usage of render prop
<DataFetcher url="/api/users">
{({ data, loading }) => (
loading ? <Spinner /> : <UserList users={data} />
)}
</DataFetcher>
The children prop enables composition - building complex UIs from simple pieces. Children can also be functions (render props pattern) for more flexible data sharing.
// Spread remaining props onto an element
function Input({ label, error, ...inputProps }) {
return (
<div className="field">
<label>{label}</label>
<input {...inputProps} />
{error && <span className="error">{error}</span>}
</div>
);
}
// All standard input props are forwarded
<Input
label="Email"
type="email"
placeholder="you@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
error={errors.email}
/>
// Avoid spreading unknown props onto DOM elements
// BAD: <div {...props} /> may pass invalid HTML attributes
// GOOD: extract known props, spread the rest carefully
The rest/spread pattern lets you create wrapper components that forward props to underlying elements. Be careful not to spread non-DOM props onto HTML elements.
Props, PropTypes, Default Props, Children Prop