Inicio / Artículos

De JSX a objetos simples: cómo funcionan los elementos, componentes y propiedades de React

Vea a qué se compila JSX, por qué los componentes son simplemente funciones, y cómo las props, los hijos, los valores predeterminados, la propagación y los slots surgen de ese mismo modelo mental.

Editor: REACTAPP.TOP9 min de lectura1983 palabras

Fuentes: JSX, Components & Props — The Mental Model Every React Developer Actually Needs

Muchos errores en React surgen de una concepción vaga sobre qué son realmente JSX, los componentes y las props. Una vez que sabes que JSX se convierte en una llamada a función, que dicha llamada genera un objeto simple y que un componente es una función que recibe un objeto con argumentos, las reglas que parecían arbitrarias comienzan a tener sentido. Este artículo construye dicho modelo partiendo de la salida del compilador y luego lo aplica a las props, children, valores por defecto, operaciones de propagación, inmutabilidad y patrones de composición que utilizarás a diario.

JSX son llamadas a función, no HTML

A menudo se denomina a JSX “HTML dentro de JavaScript”, lo cual es incompleto. Más precisamente, JSX es un azúcar sintáctico que el compilador convierte en llamadas a función ordinarias.

Toma un encabezado simple:

const element = <h1 className="title">Hello World</h1>;

Un compilador como Babel lo convierte en una llamada a React.createElement, pasando el nombre de la etiqueta, un objeto con los atributos y el contenido:

const element = React.createElement("h1", { className: "title" }, "Hello World");

Esta es la transformación clásica. Desde React 17, la mayoría de las herramientas utilizan el entorno de ejecución JSX automático, que emite llamadas a una función jsx() importada de react/jsx-runtime; por lo tanto, ya no se necesita tener a React en el ámbito de ejecución. La idea es idéntica: el marcado se convierte en una llamada a función.

Esa llamada devuelve un objeto de JavaScript puro denominado elemento React:

{
  type: "h1",
  props: {
    className: "title",
    children: "Hello World"
  }
}

Un elemento React no es más que una descripción de lo que debe aparecer en la pantalla. React recorre el árbol formado por estos objetos y determina cómo alinear el DOM real con él; ese árbol de descripciones es lo que generalmente se entiende por DOM virtual.

Qué explica el modelo

Comprender que “JSX se convierte en una llamada a función que devuelve un objeto” resuelve varios problemas comunes:

  • Sin instrucciones if dentro de JSX. No se pueden incluir instrucciones dentro de un argumento de función, por lo que se utiliza una expresión ternaria o &&, o bien la lógica se coloca antes de return.
  • className en lugar de class. JSX es JavaScript, y class es una palabra reservada allí.
  • Un único elemento raíz. Una función devuelve un solo valor, por lo que un componente debe devolver un único elemento (que puede contener muchos hijos).

Un componente es una función que devuelve elementos

Los componentes son funciones cuyo valor de retorno es un elemento de React, o null para no renderizar nada.

function Greeting() {
  return <h1>Hello, Devendra!</h1>;
}

No existe mecanismo oculto y, en React moderno, no hay necesidad de clases. React llama a la función cada vez que necesita saber qué renderizar. Cuando los datos cambian, React la llama nuevamente, recibe un nuevo árbol de elementos, lo compara con el anterior y actualiza solo las partes del DOM que difieren. Esa comparación se denomina reconciliación.

Dos reglas que debe seguir cada componente

Los nombres deben comenzar con una letra mayúscula

JSX utiliza la capitalización para determinar el significado de una etiqueta. Una etiqueta en mayúsculas se refiere a un componente, por lo que React llama a tu función:

// ✅ React treats this as a component
<Greeting />

Una etiqueta en minúsculas se trata como un elemento DOM integrado, por lo que React intentaría crear un elemento literal <greeting> en lugar de llamar a ninguna función:

// ❌ React treats this as a native DOM element (like <div>)
<greeting />

Devuelva exactamente una raíz

Devolver dos elementos hermanos uno al lado del otro falla, porque equivale a devolver dos valores desde una sola función:

// ❌ This will throw an error
function Card() {
  return (
    <h2>Title</h2>
    <p>Description</p>
  );
}

Existen dos soluciones. Envolver a los elementos hermanos en un elemento padre, o utilizar un Fragmento, escrito <>...</>, que los agrupa sin añadir un nodo adicional al DOM. El fragmento a continuación muestra ambas versiones una tras otra.

// ✅ Wrap in a parent
function Card() {
  return (
    <div>
      <h2>Title</h2>
      <p>Description</p>
    </div>
  );
}// ✅ Or use a Fragment (renders nothing extra in the DOM)
function Card() {
  return (
    <>
      <h2>Title</h2>
      <p>Description</p>
    </>
  );
}

Preferir el Fragmento a menos que se necesite el contenedor para estilos o diseño.

Los props son los argumentos de la función

Si los componentes son funciones, los props son sus argumentos. Aquí el componente lee un valor name del objeto que recibe:

function Greeting(props) {
  return <h1>Hello, {props.name}!</h1>;
}

Y aquí se renderiza con ese atributo establecido:

// Usage
<Greeting name="Devendra" />

React reúne todos los atributos escritos en la etiqueta JSX en un único objeto, props, y lo pasa como primer argumento. Ese es todo el mecanismo.

Destructuración en la firma

Repetir props. se vuelve poco claro rápidamente. Al destruir el objeto en la lista de parámetros, se nombra cada prop que se utiliza y se documentan las entradas del componente de un vistazo:

function Greeting({ name, role }) {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      <p>Role: {role}</p>
    </div>
  );
}

El lugar donde se realiza la llamada no cambia:

<Greeting name="Devendra" role="Senior Engineer" />

Valores por defecto con destructuración

Cuando un prop es opcional, asignale un valor por defecto directamente en el patrón de destructuración. Si role no se proporciona, se utilizará por defecto "Developer":

function Greeting({ name, role = "Developer" }) {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      <p>Role: {role}</p>
    </div>
  );
}

Por lo tanto, este uso muestra el rol por defecto:

// role will be "Developer" if not provided
<Greeting name="Devendra" />

La desestructuración de valores por defecto solo se aplica cuando el valor es undefined. Pasar explícitamente null evita el valor por defecto, lo cual es importante recordar cuando los propios provienen de datos de API.

children: composición integrada en la sintaxis

El contenido anidado entre las etiquetas de apertura y cierre de un componente llega automáticamente como el prop children. Un Card puede simplemente renderizar lo que se le proporcione dentro de su contenedor:

function Card({ children }) {
  return (
    <div className="card">
      {children}
    </div>
  );
}

El llamante decide qué se incluye dentro:

// Usage
<Card>
  <h2>My Title</h2>
  <p>Some description here.</p>
</Card>

La composición a través de children es la forma en que se crean componentes flexibles y reutilizables. En lugar de anticipar cada variación con otro prop, el componente proporciona la estructura y el padre suministra el contenido. Bibliotecas como shadcn/ui y Material UI construyen diálogos, tarjetas y botones de esta manera.

Cualquier valor de JavaScript puede ser una propiedad

Los llaves curvas en JSX significan “evaluar esto como JavaScript”, por lo que una propiedad puede contener cualquier valor: cadenas de texto, números, valores booleanos, arrays, objetos o funciones. Este componente recibe varios tipos a la vez, mostrando un distintivo de forma condicional, mapeando un array en elementos de lista y conectando una función de callback a un botón:

function UserCard({ name, age, isAdmin, hobbies, onDelete }) {
  return (
    <div>
      <h2>{name}</h2>
      <p>Age: {age}</p>
      {isAdmin && <span>Admin</span>}
      <ul>
        {hobbies.map(h => <li key={h}>{h}</li>)}
      </ul>
      <button onClick={onDelete}>Delete</button>
    </div>
  );
}

El lugar donde se realiza la llamada muestra cómo se pasa cada tipo:

<UserCard
  name="Devendra"
  age={28}
  isAdmin={true}
  hobbies={["coding", "teaching", "gaming"]}
  onDelete={() => console.log("deleted")}
/>

Puntos a tener en cuenta:

  • Los valores de cadena simples se pueden escribir directamente entre comillas.
  • Todo lo demás, incluidos números, valores booleanos, arrays, objetos y funciones, se coloca dentro de llaves curvas.
  • Una propiedad funcional como onDelete es la forma en que un hijo informa eventos a su padre. Este patrón de callback es lo que hace posible el manejo compartido del estado: el estado reside en un padre común, y los hijos lo modifican únicamente llamando a las funciones que el padre proporciona.
  • Las props son de solo lectura

    Nunca modifique las props. Asignar valores a un campo de un objeto prop cambia silenciosamente los datos que pertenecen al padre:

    // ❌ Never do this
    function Greeting({ user }) {
      user.name = "Modified"; // This will cause silent bugs
      return <h1>{user.name}</h1>;
    }
    

    Obtenga un nuevo valor en su lugar y deje el original intacto:

    // ✅ Create a new value instead
    function Greeting({ user }) {
      const displayName = user.name.toUpperCase();
      return <h1>{displayName}</h1>;
    }
    

    Los datos fluyen en una sola dirección, desde el padre hacia el hijo, lo cual mantiene las aplicaciones React predecibles. Un hijo que necesita que el padre modifique algo llama a una función proporcionada por el padre.

    Reenvío de props con rest y spread

    Cuando un componente envuelve un elemento DOM, a menudo se desea aceptar muchos atributos sin tener que enumerar cada uno. La sintaxis de resto en el parámetro extrae los atributos que el componente maneja por sí mismo, como label, y recopila el resto en inputProps, el cual luego se aplica al input:

    function Input({ label, ...inputProps }) {
      return (
        <div>
          <label>{label}</label>
          <input {...inputProps} />
        </div>
      );
    }
    

    Cada atributo adicional, como type, placeholder o onChange, pasa directamente al elemento subyacente:

    // All extra props (type, placeholder, onChange, etc.) flow straight to <input>
    <Input
      label="Email"
      type="email"
      placeholder="you@example.com"
      onChange={handleChange}
    />
    

    Las bibliotecas utilizan constantemente este patrón, pero asignar propiedades arbitrarias a un elemento DOM puede hacer que se transmitan valores que no entiende, lo que genera advertencias de atributos desconocidos. Sea deliberado con respecto a qué asigna y dónde. El ejemplo también muestra un label sin vincularlo al campo de entrada; en el código real, conéctelos mediante htmlFor y un id para que el label sea accesible.

    Tres niveles de flexibilidad en los componentes

    Los componentes tienden a evolucionar a través de etapas reconocibles a medida que crecen las necesidades.

    Nivel 1: una propiedad para cada variante

    El botón más simple acepta una propiedad separada para cada cosa que podría mostrar:

    function Button({ text, color, icon }) {
      return <button style={{ color }}>{icon} {text}</button>;
    }
    

    Cada nueva variante requiere otro atributo adicional, y la API crece sin límites. Este problema se conoce a menudo como explosión de atributos; es distinto de la perforación de atributos, que consiste en pasar datos a través de muchas capas.

    Nivel 2: composición de hijos

    Permitir que quienes llaman al componente pasen el contenido como children elimina la mayoría de esos atributos. El botón solo mantiene un variant para el estilo:

    function Button({ children, variant }) {
      return <button className={`btn btn-${variant}`}>{children}</button>;
    }
    

    Ahora quien llama controla lo que aparece en su interior, incluidos los iconos:

    <Button variant="primary">
      <Icon name="save" /> Save Changes
    </Button>
    

    Nivel 3: ranuras nombradas

    Cuando un componente necesita varias regiones distintas, se puede aceptar JSX a través de atributos nombrados. Un diseño de página puede incluir un encabezado, una barra lateral y contenido principal:

    function PageLayout({ header, sidebar, children }) {
      return (
        <div className="layout">
          <header>{header}</header>
          <aside>{sidebar}</aside>
          <main>{children}</main>
        </div>
      );
    }
    

    Cada región se rellena en el lugar donde se realiza la llamada:

    <PageLayout
      header={<Navbar />}
      sidebar={<FilterPanel />}
    >
      <ProductList />
    </PageLayout>
    

    Los slots son propiedades ordinarias que albergan JSX: quienes llaman a la función controlan qué llena cada área, el componente controla la estructura, y el diseño resiste los cambios en las necesidades. Para conocer más a fondo este camino de refactorización, consulte cómo solucionar la sobrecarga de propiedades en React con composición y slots.

    El modelo en resumen

    • JSX: un recurso sintáctico para llamadas a funciones que crean elementos, como React.createElement().
    • Elemento de React: un objeto simple de JavaScript que describe la interfaz de usuario.
    • Componente: una función que devuelve elementos de React.
    • Propiedades: el único objeto con argumentos que se pasa a dicha función.
    • children: una propiedad especial que contiene contenido JSX anidado.
  • Mutación de props: nunca está permitida; trate los props como de solo lectura.
  • La pureza es la regla que permite escalar

    Todo esto depende de la pureza: los mismos props siempre deben producir el mismo resultado. Cuando el renderizado no tiene efectos secundarios, nunca muta los props ni modifica el estado externo, React puede omitir, repetir o reordenar tareas de forma segura, y usted puede analizar cada componente de manera aislada. El Modo Estricto incluso renderiza los componentes dos veces en el desarrollo para detectar lógica impura.

    Si rompe la pureza, está invitando a que aparezcan errores lejos de su causa real. Si la mantiene, sus componentes seguirán siendo predecibles, fáciles de probar y mantenibles a medida que la aplicación crece.

    Lecturas relacionadas

  • Solucionar las transiciones de altura en Vue para componentes web renderizados de forma asíncrona — Aprenda por qué la transición de expansión en Vue se anima de 0px a 0px alrededor de los componentes web actualizados de forma perezosa, y cómo ResizeObserver retrasa la animación hasta que existe una altura real.