Props (short for properties) are the mechanism React uses to pass data down from a parent component to a child. They are received by the child as a plain JavaScript object and are read-only — a component must never modify its own props.
// Parent passes props
<Greeting name="Alice" age={30} isAdmin />
// Child receives them
function Greeting({ name, age, isAdmin }) {
return <p>{name} ({age}) — {isAdmin ? 'Admin' : 'User'}</p>
}
Props can be any JavaScript value: strings, numbers, booleans, objects,
arrays, or even functions and React elements. Whatever you put between
opening and closing tags flows in as children.
Rule of thumb: props flow down, never up. If a child needs to communicate to its parent, pass a callback function as a prop.
No. Props are read-only in React by design. A component is a pure function of its props (and state): given the same inputs it should always produce the same output.
function Badge({ count }) {
// ❌ Never do this
count = count + 1
// ✅ Derive a new value without mutating
const displayCount = count > 99 ? '99+' : count
return <span>{displayCount}</span>
}
If you try to reassign a prop it may work locally (it's just a JS variable), but the change won't persist across renders and will confuse React's reconciler because the parent still owns the original value.
Rule of thumb: treat every prop as a const. To derive a modified
value, create a new local variable; to change what the parent passes, fire
a callback that updates the parent's state.
children is the implicit prop that React populates with whatever you put
between a component's JSX tags. It enables composition — parent decides
the shell, child slot receives arbitrary content.
function Panel({ title, children }) {
return (
<section className="panel">
<h2>{title}</h2>
<div className="panel-body">{children}</div>
</section>
)
}
// Usage
<Panel title="Stats">
<p>Revenue: $1,200</p>
<Chart data={data} />
</Panel>
children can be a string, a single element, an array of elements, a
function (render prop), or undefined (nothing passed). Use
React.Children.count(children) or React.Children.toArray(children) to
inspect it programmatically.
Rule of thumb: children is just a prop with special JSX sugar. It
lets you build generic "container" components that don't need to know what
goes inside them.
| Function component | Class component | |
|---|---|---|
| Syntax | Plain JS function | class X extends React.Component |
| State | useState, useReducer |
this.state / this.setState |
| Lifecycle | useEffect |
componentDidMount, componentDidUpdate, etc. |
| Context | useContext |
static contextType / Context.Consumer |
this |
Not applicable | Required for everything |
| Error boundaries | Not supported | componentDidCatch |
// Function component
function Counter() {
const [n, setN] = useState(0)
return <button onClick={() => setN(n + 1)}>{n}</button>
}
// Class component — equivalent
class Counter extends React.Component {
state = { n: 0 }
render() {
return <button onClick={() => this.setState({ n: this.state.n + 1 })}>{this.state.n}</button>
}
}
Since React 16.8 (hooks), function components can do everything class components can except implement error boundaries. New code should use function components.
Rule of thumb: always write function components. Only reach for a class
when you need a componentDidCatch error boundary — or when you're
maintaining legacy code.
The only built-in feature class components have that function components
lack is the ability to serve as an error boundary via
componentDidCatch and getDerivedStateFromError. There is no hooks-based
equivalent in React itself (as of 2024).
class ErrorBoundary extends React.Component {
state = { hasError: false }
static getDerivedStateFromError() {
return { hasError: true }
}
componentDidCatch(err, info) {
logError(err, info)
}
render() {
return this.state.hasError
? <p>Something went wrong.</p>
: this.props.children
}
}
In practice most teams write a single ErrorBoundary class component and
reach for third-party wrappers like react-error-boundary everywhere else.
Rule of thumb: one class component per project (the error boundary). Everything else is a function component.
The modern idiomatic approach is default parameter destructuring:
function Button({ label = 'Click me', variant = 'primary', disabled = false }) {
return <button className={variant} disabled={disabled}>{label}</button>
}
Alternatively, you can use the defaultProps static property, but this is
considered legacy and may be removed in a future major version:
Button.defaultProps = {
label: 'Click me',
variant: 'primary',
disabled: false,
}
For TypeScript users, the destructuring approach integrates cleanly with type annotations:
type ButtonProps = { label?: string; variant?: 'primary' | 'secondary' }
function Button({ label = 'Click me', variant = 'primary' }: ButtonProps) { … }
Rule of thumb: use destructuring defaults; they're co-located with the parameter list and work without any React-specific API.
PropTypes is a runtime prop validation library that ships separately
(prop-types package). In development mode it logs warnings when props
fail the declared type, catching mistakes early.
import PropTypes from 'prop-types'
function Avatar({ name, size, src }) {
return <img src={src} alt={name} width={size} />
}
Avatar.propTypes = {
name: PropTypes.string.isRequired,
size: PropTypes.number,
src: PropTypes.string.isRequired,
}
Avatar.defaultProps = { size: 48 }
PropTypes are stripped in production builds. The trade-off vs TypeScript:
- PropTypes — runtime, no build-step, catches type mistakes at render time in any JS project.
- TypeScript — compile-time, catches mistakes before runtime, broader coverage (not just props).
Rule of thumb: in a TypeScript project, TypeScript alone is sufficient. In a plain JS project, PropTypes are a lightweight safety net.
| Props | State | |
|---|---|---|
| Owned by | Parent component | Component itself |
| Mutable by | Parent only | The component (setState / setter) |
| Triggers re-render | When parent re-renders | When updated via setter |
| Purpose | Pass data in | Track internal data that changes |
function Thermometer({ unit }) { // unit is a prop — parent decides
const [temp, setTemp] = useState(20) // temp is state — component owns it
return (
<div>
{temp}° {unit}
<button onClick={() => setTemp(t => t + 1)}>+</button>
</div>
)
}
A useful mental model: props are like function arguments; state is like local variables that persist between calls.
Rule of thumb: if a value is determined by the outside world, it's a prop. If it's determined by the component itself and can change, it's state.
Prop drilling occurs when a prop must be passed through several intermediate layers that don't use it — just to get it to a deeply nested component.
// Drilling `user` through three layers that don't care about it
<App user={user}>
<Layout user={user}>
<Sidebar user={user}>
<Avatar user={user} /> {/* ← only Avatar actually uses it */}
</Sidebar>
</Layout>
</App>
Alternatives:
- Context API —
React.createContext+useContext. Best for genuinely global or cross-cutting data (theme, current user, locale). - Component composition / children — pass
<Avatar user={user} />as a child instead of passinguserthrough intermediaries. - State management library — Zustand, Redux, Jotai — good when many unrelated components need the same data.
Rule of thumb: before reaching for Context, try restructuring with composition. Context adds implicit coupling; explicit props are easier to trace.
Destructure props directly in the function signature rather than accessing
them via the props object — it's more concise and makes required fields
obvious.
// Verbose
function Card(props) {
return <div className={props.variant}>{props.children}</div>
}
// Destructured — preferred
function Card({ variant, children }) {
return <div className={variant}>{children}</div>
}
// With defaults and rest
function Card({ variant = 'default', className = '', children, ...rest }) {
return (
<div className={`card card-${variant} ${className}`} {...rest}>
{children}
</div>
)
}
Rule of thumb: destructure all props at the function signature. For
large prop objects, consider grouping related props into sub-objects
(position={{ x, y }}) to keep the signature readable.
Pass the function reference as a prop, not the function call. The child receives it and calls it when the event fires.
// Parent
function Form() {
function handleSubmit(data) {
console.log(data)
}
return <SubmitButton onSubmit={handleSubmit} label="Save" />
}
// Child — receives and calls the handler
function SubmitButton({ onSubmit, label }) {
return <button onClick={() => onSubmit({ time: Date.now() })}>{label}</button>
}
Convention: name callback props with the on prefix (onChange, onClose,
onSubmit) to signal that they are event-like.
Rule of thumb: pass function references, not calls. onClick={handle}
passes the function; onClick={handle()} calls it immediately and passes
its return value — almost never what you want.
Spread props ({...rest}) forward a batch of unknown props to a child
without enumerating each one. It's most useful in wrapper/adapter
components.
function Input({ label, error, ...inputProps }) {
// label and error are consumed here; everything else goes to <input>
return (
<div>
<label>{label}</label>
<input {...inputProps} />
{error && <span className="error">{error}</span>}
</div>
)
}
// Usage — all native <input> attributes work without any boilerplate
<Input label="Email" type="email" name="email" required error={errors.email} />
Caution: spreading onto DOM elements can forward non-standard props, which React will warn about. Always destructure known props first and spread the remainder.
Rule of thumb: spread ...rest to DOM wrappers; avoid spreading the
full props object without filtering — it leaks internal props.
A pure component is one whose output depends only on its props (and state), with no side effects during rendering. Given the same inputs it always returns the same JSX — like a pure function in FP.
In class components, React.PureComponent adds a shallow-equality check
in shouldComponentUpdate, preventing re-renders when props haven't
changed.
In function components, React.memo provides the same shallow-equality
bail-out:
// Without memo — re-renders every time parent re-renders
function Label({ text }) {
return <span>{text}</span>
}
// With memo — skips re-render if text hasn't changed
const Label = React.memo(function Label({ text }) {
return <span>{text}</span>
})
Rule of thumb: apply React.memo only to components that render
frequently with the same props and are measurably expensive. Premature
memoization adds complexity without benefit — profile first.
React's component model is built around composition: assembling behavior
by combining components, not by extending them. The React team explicitly
recommends against using inheritance between components (other than extending
React.Component once).
Composition approaches:
- Children prop — slot any content into a component.
- Specialized components — make a
<Dialog>and then a<AlertDialog>that renders a pre-configured<Dialog>, not one that extends it. - Render props / HOCs — share logic without shared class hierarchies.
// ✅ Composition
function SuccessDialog({ children }) {
return <Dialog icon="check" title="Success">{children}</Dialog>
}
// ❌ Inheritance — unusual in React, actively discouraged
class SuccessDialog extends Dialog { … }
Rule of thumb: when you think "I want a component like X but with Y changed," reach for composition (wrap, extend via props, or use children) before ever reaching for class inheritance.
A controlled component is a form element whose value is driven by React state rather than by the DOM. React is the single source of truth.
function LoginForm() {
const [email, setEmail] = useState('')
return (
<input
type="email"
value={email} // state drives the displayed value
onChange={e => setEmail(e.target.value)} // state updated on change
/>
)
}
Every keystroke calls setEmail, which updates state, which re-renders,
which sets value — a tight loop. This means you can validate, transform,
or intercept input on every change.
The alternative, uncontrolled components, read values from the DOM via a ref at submission time rather than tracking each change.
Rule of thumb: use controlled inputs when you need real-time validation or to format input as the user types; uncontrolled when the form is simple and you only need values on submit.
A render prop is a prop whose value is a function, allowing one component to delegate rendering decisions to its parent. It's a technique for sharing stateful logic without hooks or HOCs.
// Provider of the logic
function MouseTracker({ render }) {
const [pos, setPos] = useState({ x: 0, y: 0 })
return (
<div onMouseMove={e => setPos({ x: e.clientX, y: e.clientY })}>
{render(pos)} {/* call the function to get JSX */}
</div>
)
}
// Consumer decides what to display
<MouseTracker render={({ x, y }) => (
<p>Mouse at {x}, {y}</p>
)} />
The children prop can be used the same way (and often is):
<MouseTracker>
{({ x, y }) => <p>Mouse at {x}, {y}</p>}
</MouseTracker>
In modern code, custom hooks replace most render-prop use cases with less indirection.
Rule of thumb: know the pattern for interviews; in practice, custom hooks are almost always cleaner.
More Components interview questions
More ways to practice
The self-quiz is live. Get notified when mock interviews and new question packs drop.