React – Props

June 21, 20264 min readUpdated 8/18/2026

Props are how a component takes arguments. You pass them like HTML attributes, and the component receives them as one object.

// Passing
<ProductCard product={product} onSelect={handleSelect} />

// Receiving
function ProductCard(props) {
  return <div>{props.product.name}</div>;
}

In practice nobody writes props. everywhere. Destructure in the signature:

function ProductCard({ product, onSelect }) {
  return <div>{product.name}</div>;
}

Typing them

Props are the one place TypeScript consistently earns its keep in a React codebase — it is the contract between two files. Two styles, both common:

// Inline, for one or two props.
export function AppNavbar({ onOpenCart }: { onOpenCart: () => void }) { /* … */ }

// A named interface, once there are more or they are reused.
interface Props {
  product: Product;
  onSelect: (product: Product) => void;
}

export function ProductCard({ product, onSelect }: Props) { /* … */ }

Note the type of onSelect: (product: Product) => void. A function prop is typed like any other function. Get the arity or the argument type wrong at the call site and the build fails rather than the click doing nothing at runtime.

Optional props get a ?, and usually a default:

export function ProtectedRoute({
  children,
  requireAdmin = false,
}: {
  children: ReactNode;
  requireAdmin?: boolean;
}) { /* … */ }

The default goes in the destructuring, not in the type. requireAdmin?: boolean says the caller may omit it; requireAdmin = false says what happens when they do. Both are needed.

Props are read-only

This is the rule that explains a large share of React confusion.

function ProductCard({ product }: Props) {
  product.name = 'Something else';   // WRONG. Do not do this.
  // …
}

A component must not modify its props. It is not that React forbids it loudly — in plain JavaScript that assignment succeeds — it is that the parent owns that data and has no idea you changed it. The parent will not re-render, other components reading the same object will disagree about its contents, and you will spend an afternoon on it.

If a component needs to change something, that something belongs in state, owned by whoever is highest in the tree that cares. See State with useState.

Talking back to the parent: function props

Data flows down. When a child needs to cause something upward, the parent passes down a function and the child calls it.

The menu page owns which product is being configured. The card does not — it only knows one was clicked:

// MenuPage.tsx — the parent owns the state and hands down a way to change it.
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);

const handleSelect = useCallback((product: Product) => {
  setSelectedProduct(product);
}, []);

return (
  <Row xs={1} sm={2} lg={4} className="g-4">
    {visibleProducts.map((product) => (
      <Col key={product.id}>
        <ProductCard product={product} onSelect={handleSelect} />
      </Col>
    ))}
  </Row>
);
// ProductCard.tsx — the child reports the event and knows nothing about modals.
<Button size="sm" variant="primary" onClick={() => onSelect(product)}>
  {isPizza ? 'Build it' : 'Add'}
</Button>

This is the pattern everywhere: onSelect, onOpenCart, onHide. The on… naming is convention, not syntax, but follow it — it is how a reader knows a prop is an event rather than data.

The child stays reusable precisely because it does not know what happens next. Drop ProductCard on the home page and pass a different onSelect, and it works.

The children prop

Anything you put between a component's tags arrives as a prop named children:

<ProtectedRoute requireAdmin>
  <AdminLayout />
</ProtectedRoute>

<AdminLayout /> is props.children. The wrapper decides whether and where to render it:

export function ProtectedRoute({ children, requireAdmin = false }: Props) {
  const { isAuthenticated, isAdmin, initialising } = useAuth();
  const location = useLocation();

  if (initialising) return <Spinner />;
  if (!isAuthenticated) return <Navigate to="/login" state={{ from: location }} replace />;
  if (requireAdmin && !isAdmin) return <Navigate to="/" replace />;

  return <>{children}</>;
}

That is a component whose whole job is to decide whether to render its children. Error boundaries, providers, layouts and modals all work this way — it is the main tool React gives you for wrapping behaviour around arbitrary content.

Type it as ReactNode, which covers elements, strings, numbers, arrays and null:

import type { ReactNode } from 'react';

interface Props {
  children: ReactNode;
}

children does not have to be markup. It can be a function, an object, anything — props are just an object and children is just a key. In practice it is markup essentially always.

What should be a prop?

Pass what the child needs, at the level of abstraction it works at.

ProductCard takes the whole product rather than name, description, sizes and type as four props — they always travel together and the card is about a product. But AppNavbar takes only onOpenCart, not the cart itself, because it reads the cart from context.

That last part matters. If you find yourself passing a prop through three components that do not use it, just to reach the fourth, you have hit prop drilling — and the answer is Context, not more props.

Next

Conditional Rendering — showing something only sometimes.