Skip to content

Props and Component Types Interview Questions & Answers

16 questions Updated 2026-06-23 Share:

React props and component types interview questions — function vs class components, children, defaultProps, PropTypes, composition over inheritance, and controlled components.

Read the in-depth guideReact Props and Component Types — A Complete Interview Guide(opens in new tab)
16 of 16

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:

  1. Context APIReact.createContext + useContext. Best for genuinely global or cross-cutting data (theme, current user, locale).
  2. Component composition / children — pass <Avatar user={user} /> as a child instead of passing user through intermediaries.
  3. 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 ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel