← React & TypeScript

React with TypeScript

How do you type `children` in a component and what is composition for in React?

Show answer — try answering out loud first

children is typed as ReactNode, the broadest type: it accepts JSX, strings, numbers, arrays, or null. Composition consists of building interfaces by putting components inside others, instead of creating giant components full of props.

When you write <Tarjeta>Hola</Tarjeta>, whatever goes between the tags reaches the component as the children prop. It's the foundation of composition: the container decides the frame (border, padding, title) and whoever uses it decides the content.

The key points:

  • ReactNode is the right type for children: it covers everything React can render (JSX elements, strings, numbers, arrays, null, undefined).
  • Not just children: any prop can receive a component. For example, icono: ReactNode lets you pass <IconoEstrella /> to a card.
  • Difference between similar types:
    • ReactNode: anything renderable. Use it almost always.
    • ReactElement: only a JSX element (<div />, <Boton />). A string no longer fits. Use it when you truly require JSX.
    • string: only plain text. Useful if you're going to manipulate the content as text.
  • Composition avoids the "God component" with 20 props: the container receives children and each screen fills it differently.
import type { ReactNode, ReactElement } from "react";

// Container component: sets the frame, doesn't decide the content
type TarjetaProps = {
  titulo: string;      // just text: we use it as-is
  icono?: ReactNode;   // a component passed via props
  children: ReactNode; // anything renderable
};

function Tarjeta({ titulo, icono, children }: TarjetaProps) {
  return (
    <section className="tarjeta">
      <header>{icono}<h2>{titulo}</h2></header>
      <div className="tarjeta-cuerpo">{children}</div>
    </section>
  );
}

// ReactElement: here we require JSX, a string won't compile
function Modal({ contenido }: { contenido: ReactElement }) {
  return <div className="modal">{contenido}</div>;
}

// Usage: the same container works for very different contents
// <Tarjeta titulo="Perfil" icono={<span aria-hidden>*</span>}>
//   <p>Nombre: Ana</p>
// </Tarjeta>
// <Tarjeta titulo="Aviso">Solo un texto simple</Tarjeta>
// <Modal contenido={<p>Confirmar acción</p>} />
// <Modal contenido="hola" />
// Error: Type 'string' is not assignable to type 'ReactElement'

Typing children as string or as JSX.Element and falling short:

type TarjetaProps = { children: string }; // too restrictive

// <Tarjeta><p>Hola</p></Tarjeta>
// Error: Type 'Element' is not assignable to type 'string'

With string you can't pass JSX, and with ReactElement you can't freely pass loose text or multiple children. The fix is to use the type designed exactly for that:

import type { ReactNode } from "react";

type TarjetaProps = { children: ReactNode }; // accepts text, JSX, arrays, null...
// Mental rule: for children, ReactNode is the default choice

"I type children as ReactNode, because it's the type that covers everything renderable: JSX, strings, numbers, arrays, or null. ReactElement is stricter, it only accepts JSX elements, and I reserve it for when I truly require an element. Composition helps me avoid creating components with twenty props: I make containers like <Tarjeta> that set the frame and receive the content via children, and if I need specific slots, I pass components via props, like an icono: ReactNode. That way I reuse the container across the whole app."

Quick challenge

Create a Panel component that receives children and an optional piePagina prop (also renderable). The footer is only shown if it's provided.

See solution
import type { ReactNode } from "react";

type PanelProps = {
  children: ReactNode;
  piePagina?: ReactNode;
};

function Panel({ children, piePagina }: PanelProps) {
  return (
    <section className="panel">
      <div className="panel-cuerpo">{children}</div>
      {piePagina && <footer className="panel-pie">{piePagina}</footer>}
    </section>
  );
}

// Usage: <Panel piePagina={<small>Actualizado hoy</small>}>...</Panel>