Cómo crear un componente React desde cero paso a paso

D
DanisCh
(Actualizado: ) • 13 min de lectura
Cómo crear un componente React desde cero paso a paso
HTML JavaScript

React organiza las interfaces de usuario en componentes: piezas de código reutilizables que combinan HTML, CSS y lógica en una sola unidad. En lugar de tener una página enorme con todo mezclado, una aplicación React es un árbol de componentes que se combinan para construir la interfaz completa. Entender cómo crear y estructurar componentes es el primer paso real para trabajar con React.

En esta guía crearás varios componentes desde cero, aprenderás a pasarles datos mediante props, a manejar estado con hooks y a conectar todo en una aplicación funcional.

Qué es un componente en React

Un componente de React es una función JavaScript que devuelve JSX: una sintaxis que se parece al HTML pero que en realidad es JavaScript. React toma ese JSX, lo convierte en elementos del DOM real y los muestra en el navegador.

// El componente más simple posible
function Saludo() {
  return <h1>Hola, mundo</h1>;
}

// Usarlo en otro componente:
function App() {
  return (
    <div>
      <Saludo />
    </div>
  );
}

Dos reglas que debes recordar siempre:

  • El nombre del componente siempre empieza en mayúscula. React distingue entre <div> (elemento HTML nativo) y <Div> (componente de React).
  • Un componente solo puede devolver un elemento raíz. Si necesitas devolver varios, envuélvelos en un <div> o en un Fragment (<>...</>).
// ❌ Error: dos elementos raíz
function Componente() {
  return (
    <h1>Título</h1>
    <p>Párrafo</p>
  );
}

// ✅ Envueltos en un fragment (no añade nodo al DOM)
function Componente() {
  return (
    <>
      <h1>Título</h1>
      <p>Párrafo</p>
    </>
  );
}

JSX: HTML dentro de JavaScript

JSX se parece al HTML pero tiene algunas diferencias importantes que debes conocer antes de seguir:

// 1. class se llama className (class es palabra reservada en JavaScript)
<div className="contenedor">...</div>

// 2. Las etiquetas siempre deben cerrarse (incluyendo las que en HTML no se cierran)
<img src="foto.jpg" alt="Foto" />
<input type="text" />
<br />

// 3. Las expresiones JavaScript van entre llaves {}
const nombre = "Ana";
const edad = 25;

function Perfil() {
  return (
    <div>
      <h2>{nombre}</h2>           {/* variable */}
      <p>Edad: {edad} años</p>    {/* expresión */}
      <p>Próximo año: {edad + 1}</p>  {/* operación */}
      <p>{nombre.toUpperCase()}</p>   {/* llamada a función */}
    </div>
  );
}

// 4. Los comentarios en JSX van dentro de llaves
function Componente() {
  return (
    <div>
      {/* Esto es un comentario en JSX */}
      <p>Contenido</p>
    </div>
  );
}

// 5. style recibe un objeto, no un string
<div style={{ backgroundColor: 'blue', fontSize: '18px' }}>...</div>
// Las propiedades CSS usan camelCase: backgroundColor, fontSize, marginTop

Props: pasar datos a un componente

Las props (propiedades) son la forma en que los componentes reciben datos de su componente padre. Son como los parámetros de una función.

// Definir un componente que recibe props
function TarjetaUsuario(props) {
  return (
    <div className="tarjeta">
      <img src={props.avatar} alt={props.nombre} />
      <h3>{props.nombre}</h3>
      <p>{props.email}</p>
    </div>
  );
}

// Usarlo pasando las props
function App() {
  return (
    <div>
      <TarjetaUsuario
        nombre="Ana García"
        email="ana@mail.com"
        avatar="https://ejemplo.com/avatar-ana.jpg"
      />
      <TarjetaUsuario
        nombre="Carlos López"
        email="carlos@mail.com"
        avatar="https://ejemplo.com/avatar-carlos.jpg"
      />
    </div>
  );
}
// Desestructurar las props (forma más común y limpia)
function TarjetaUsuario({ nombre, email, avatar, activo = true }) {
  //                                                ↑ valor por defecto
  return (
    <div className={`tarjeta ${activo ? 'activo' : 'inactivo'}`}>
      <img src={avatar} alt={nombre} />
      <h3>{nombre}</h3>
      <p>{email}</p>
      <span>{activo ? '✓ Activo' : '✗ Inactivo'}</span>
    </div>
  );
}

// Con TypeScript: definir los tipos de las props
interface TarjetaUsuarioProps {
  nombre: string;
  email:  string;
  avatar: string;
  activo?: boolean;   // ? = opcional
}

function TarjetaUsuario({ nombre, email, avatar, activo = true }: TarjetaUsuarioProps) {
  // ...
}

La prop children

// children es el contenido que se pone entre las etiquetas del componente
function Tarjeta({ titulo, children }) {
  return (
    <div className="tarjeta">
      <h3 className="tarjeta__titulo">{titulo}</h3>
      <div className="tarjeta__cuerpo">
        {children}    {/* aquí se renderiza el contenido interior */}
      </div>
    </div>
  );
}

// Uso: el contenido entre las etiquetas se convierte en children
function App() {
  return (
    <Tarjeta titulo="Mi primera tarjeta">
      <p>Este es el contenido de la tarjeta.</p>
      <button>Acción</button>
    </Tarjeta>
  );
}

Estado con useState

Las props son datos que vienen del exterior y no cambian dentro del componente. El estado (state) son datos que el componente gestiona internamente y que pueden cambiar con el tiempo. Cuando el estado cambia, React re-renderiza el componente automáticamente.

import { useState } from 'react';

function Contador() {
  // useState devuelve [valorActual, funcionParaCambiarlo]
  const [cuenta, setCuenta] = useState(0);  // 0 es el valor inicial

  return (
    <div>
      <p>Cuenta: {cuenta}</p>
      <button onClick={() => setCuenta(cuenta + 1)}>Aumentar</button>
      <button onClick={() => setCuenta(cuenta - 1)}>Disminuir</button>
      <button onClick={() => setCuenta(0)}>Resetear</button>
    </div>
  );
}
// Regla importante: NUNCA modifiques el estado directamente
// Siempre usa la función setter que devuelve useState

// ❌ Esto no funciona: React no detecta el cambio y no re-renderiza
cuenta = cuenta + 1;

// ✅ Esto funciona: React detecta el cambio y actualiza la UI
setCuenta(cuenta + 1);

// Cuando el nuevo valor depende del anterior, usa la forma funcional
// (más segura porque garantiza que usas el valor más reciente)
setCuenta(valorAnterior => valorAnterior + 1);
// Estado con objetos: siempre crear un nuevo objeto, no mutar el existente
function FormularioUsuario() {
  const [usuario, setUsuario] = useState({
    nombre: '',
    email:  '',
    edad:   ''
  });

  function actualizarCampo(campo, valor) {
    setUsuario({
      ...usuario,        // copiar todos los campos actuales
      [campo]: valor     // sobrescribir solo el campo que cambió
    });
  }

  return (
    <form>
      <input
        value={usuario.nombre}
        onChange={e => actualizarCampo('nombre', e.target.value)}
        placeholder="Nombre"
      />
      <input
        value={usuario.email}
        onChange={e => actualizarCampo('email', e.target.value)}
        placeholder="Email"
      />
      <p>Hola, {usuario.nombre || 'desconocido'}</p>
    </form>
  );
}

Renderizado condicional

function Alerta({ tipo, mensaje, visible }) {
  // Si no es visible, no renderizar nada
  if (!visible) return null;

  return (
    <div className={`alerta alerta--${tipo}`}>
      {mensaje}
    </div>
  );
}

// Formas habituales de renderizado condicional en JSX:

function Componente({ cargando, error, datos }) {
  return (
    <div>
      {/* Operador ternario: condición ? siVerdadero : siFalso */}
      {cargando ? <Spinner /> : <Contenido datos={datos} />}

      {/* Operador &&: solo renderiza si la condición es verdadera */}
      {error && <p className="error">{error}</p>}

      {/* Operador ||: valor por defecto */}
      <p>{datos?.nombre || 'Sin nombre'}</p>

      {/* Operador ??: nullish coalescing (solo para null/undefined) */}
      <p>{datos?.edad ?? 'Edad desconocida'}</p>
    </div>
  );
}
// Cuidado con el 0: en JSX, 0 se renderiza como texto
// ❌ Si lista.length es 0, renderiza "0" en pantalla
{lista.length && <ul>...</ul>}

// ✅ Convertir a booleano explícitamente
{lista.length > 0 && <ul>...</ul>}
{!!lista.length && <ul>...</ul>}

Renderizar listas

function ListaProductos({ productos }) {
  return (
    <ul>
      {productos.map(producto => (
        // La prop 'key' es obligatoria: ayuda a React a identificar qué elemento cambió
        // Debe ser única entre hermanos y estable (no usar el índice si la lista puede reordenarse)
        <li key={producto.id}>
          <strong>{producto.nombre}</strong> — {producto.precio}€
        </li>
      ))}
    </ul>
  );
}

// Uso
const productos = [
  { id: 1, nombre: 'Teclado',  precio: 79 },
  { id: 2, nombre: 'Ratón',    precio: 39 },
  { id: 3, nombre: 'Monitor',  precio: 299 },
];

<ListaProductos productos={productos} />
// Lista con componente separado para cada ítem (mejor práctica)
function ItemProducto({ producto, onEliminar }) {
  return (
    <li className="item-producto">
      <span>{producto.nombre}</span>
      <span>{producto.precio}€</span>
      <button onClick={() => onEliminar(producto.id)}>Eliminar</button>
    </li>
  );
}

function ListaProductos({ productos, onEliminarProducto }) {
  if (productos.length === 0) {
    return <p>No hay productos</p>;
  }

  return (
    <ul>
      {productos.map(producto => (
        <ItemProducto
          key={producto.id}
          producto={producto}
          onEliminar={onEliminarProducto}
        />
      ))}
    </ul>
  );
}

Manejo de eventos

// Los eventos en React son camelCase y reciben funciones, no strings
function Boton() {
  function manejarClic(evento) {
    console.log('Clic!', evento.target);
  }

  return <button onClick={manejarClic}>Hacer clic</button>;
}

// Pasar argumentos a los manejadores
function Lista({ items }) {
  function manejarClic(id) {
    console.log('Seleccionado:', id);
  }

  return (
    <ul>
      {items.map(item => (
        // La función flecha crea una función nueva que llama a manejarClic con el id
        <li key={item.id} onClick={() => manejarClic(item.id)}>
          {item.nombre}
        </li>
      ))}
    </ul>
  );
}

// Eventos habituales en React
<button onClick={fn}>Clic</button>
<input onChange={fn} />       {/* cada vez que cambia el valor */}
<input onFocus={fn} />        {/* al recibir el foco */}
<input onBlur={fn} />         {/* al perder el foco */}
<form onSubmit={fn}></form>   {/* al enviar el formulario */}
<div onMouseEnter={fn} />     {/* al entrar el ratón */}
<div onKeyDown={fn} />        {/* al pulsar una tecla */}

Componente completo: lista de tareas

Combinando todo lo anterior, aquí tienes una aplicación de lista de tareas completa y funcional:

import { useState } from 'react';

// Componente para una tarea individual
function ItemTarea({ tarea, onCompletar, onEliminar }) {
  return (
    <li className={`tarea ${tarea.completada ? 'completada' : ''}`}>
      <input
        type="checkbox"
        checked={tarea.completada}
        onChange={() => onCompletar(tarea.id)}
      />
      <span className="tarea__texto">{tarea.texto}</span>
      <button
        className="tarea__eliminar"
        onClick={() => onEliminar(tarea.id)}
        aria-label="Eliminar tarea"
      >
        ✕
      </button>
    </li>
  );
}

// Componente para añadir nuevas tareas
function FormularioTarea({ onAñadir }) {
  const [texto, setTexto] = useState('');

  function manejarEnvio(e) {
    e.preventDefault();           // evitar recarga de página
    if (!texto.trim()) return;    // no añadir tareas vacías
    onAñadir(texto.trim());
    setTexto('');                 // limpiar el input
  }

  return (
    <form onSubmit={manejarEnvio} className="formulario">
      <input
        type="text"
        value={texto}
        onChange={e => setTexto(e.target.value)}
        placeholder="Nueva tarea..."
        className="formulario__input"
      />
      <button type="submit" className="formulario__boton">
        Añadir
      </button>
    </form>
  );
}

// Componente principal que une todo
function ListaTareas() {
  const [tareas, setTareas] = useState([
    { id: 1, texto: 'Aprender React', completada: true },
    { id: 2, texto: 'Crear mi primer componente', completada: false },
    { id: 3, texto: 'Entender los hooks', completada: false },
  ]);

  function añadirTarea(texto) {
    const nueva = {
      id:         Date.now(),   // id único basado en el timestamp
      texto,
      completada: false,
    };
    setTareas([...tareas, nueva]);  // nuevo array con la tarea añadida
  }

  function completarTarea(id) {
    setTareas(tareas.map(tarea =>
      tarea.id === id
        ? { ...tarea, completada: !tarea.completada }  // invertir completada
        : tarea                                          // sin cambios
    ));
  }

  function eliminarTarea(id) {
    setTareas(tareas.filter(tarea => tarea.id !== id));
  }

  const pendientes  = tareas.filter(t => !t.completada).length;
  const completadas = tareas.filter(t =>  t.completada).length;

  return (
    <div className="lista-tareas">
      <h1>Mis tareas</h1>
      <p className="resumen">
        {pendientes} pendiente{pendientes !== 1 ? 's' : ''} ·{' '}
        {completadas} completada{completadas !== 1 ? 's' : ''}
      </p>

      <FormularioTarea onAñadir={añadirTarea} />

      {tareas.length === 0 ? (
        <p className="vacio">No hay tareas. ¡Añade una!</p>
      ) : (
        <ul className="lista">
          {tareas.map(tarea => (
            <ItemTarea
              key={tarea.id}
              tarea={tarea}
              onCompletar={completarTarea}
              onEliminar={eliminarTarea}
            />
          ))}
        </ul>
      )}
    </div>
  );
}

export default ListaTareas;

Cómo organizar los componentes en archivos

// Cada componente en su propio archivo es la convención estándar // src/components/TarjetaUsuario.jsx export function TarjetaUsuario({ nombre, email, avatar }) {  return (    <div className="tarjeta-usuario">      <img src={avatar} alt={nombre} />      <h3>{nombre}</h3>      <p>{email}</p>    </div>  ); } // src/components/TarjetaUsuario.css (o TarjetaUsuario.module.css) // .tarjeta-usuario { ... } // src/App.jsx import { TarjetaUsuario } from './components/TarjetaUsuario'; function App() {  return <TarjetaUsuario nombre="Ana" email="ana@mail.com" avatar="..." />; } // Estructura de carpetas habitual para un proyecto pequeño-mediano: // src/ //   components/       ← componentes reutilizables //     TarjetaUsuario/ //       index.jsx //       TarjetaUsuario.module.css //   pages/            ← componentes de página //     Home.jsx //     Perfil.jsx //   hooks/            ← custom hooks //   utils/            ← funciones de utilidad //   App.jsx //   main.jsx

Los errores más comunes al crear componentes

// ❌ Error 1: modificar el estado directamente
const [lista, setLista] = useState([1, 2, 3]);
lista.push(4);   // React no se entera del cambio

// ✅ Crear un nuevo array
setLista([...lista, 4]);


// ❌ Error 2: olvidar la prop key en las listas
{items.map(item => <li>{item.nombre}</li>)}
// Warning: Each child in a list should have a unique "key" prop.

// ✅ Siempre añadir key única y estable
{items.map(item => <li key={item.id}>{item.nombre}</li>)}


// ❌ Error 3: llamar a la función en el onClick en lugar de pasarla
<button onClick={manejarClic()}>   {/* se ejecuta al renderizar, no al hacer clic */}

// ✅ Pasar la referencia a la función
<button onClick={manejarClic}>
// ✅ O con función flecha si necesitas pasar argumentos
<button onClick={() => manejarClic(id)}>


// ❌ Error 4: usar el índice como key cuando la lista puede reordenarse
{items.map((item, index) => <li key={index}>{item}</li>)}
// Causa bugs difíciles de detectar: React confunde los elementos al reordenar

// ✅ Usar un identificador único del dato
{items.map(item => <li key={item.id}>{item.nombre}</li>)}

Resumen

  • Un componente React es una función que devuelve JSX. El nombre siempre empieza en mayúscula y solo puede devolver un elemento raíz (usa <></> si necesitas varios sin añadir nodo extra).
  • JSX se parece al HTML pero tiene diferencias: className en lugar de class, todas las etiquetas deben cerrarse, las expresiones JavaScript van entre {} y style recibe un objeto con camelCase.
  • Las props son datos que el componente padre pasa al hijo. Son de solo lectura: el componente hijo nunca debe modificarlas. Desectructúralas en los parámetros de la función para un código más limpio.
  • El estado (useState) son datos que el componente gestiona internamente y que pueden cambiar. Cuando cambian, React re-renderiza el componente. Nunca modifiques el estado directamente: usa siempre la función setter.
  • Para renderizar listas usa .map() y añade siempre la prop key con un identificador único y estable del dato, no el índice del array.
  • El patrón más habitual para componentes complejos es: el componente padre tiene el estado y las funciones que lo modifican, y los pasa a los hijos como props. Los hijos llaman a esas funciones cuando el usuario interactúa.

¿Te ha gustado esta entrada?

Compártela con tus compañeros para que también sigan aprendiendo.

Comunidad y Comentarios

0 COMENTARIOS

No hay comentarios todavía. Sé el primero en compartir tu opinión.

Escribe tu opinión
Respondiendo a