Skip to content
This page has been auto-translated and may contain errors.View in English

Pensar en React

Cada capítulo hasta ahora ha enseñado una pieza: componentes, props, estado, eventos, listas. Este capítulo es donde se unen en un método. Dado un diseño, ¿cómo decides qué son los componentes, cuáles valores son estado y dónde debe vivir ese estado? Hay una forma repetible de trabajar a través de esto, y una vez que lo hayas hecho algunas veces, recurrirás a los mismos cinco pasos en casi todas las pantallas que construyas.

Vamos a recorrer una interfaz pequeña de punta a punta: una lista de productos que se puede buscar. Muestra un conjunto de productos, una caja de búsqueda para filtrarlos por nombre, y una casilla para ocultar cualquier cosa que no esté en stock. Aquí están los datos que renderiza:

jsx
const PRODUCTS = [
  { id: 1, name: 'Manzana', price: '$1', stocked: true },
  { id: 2, name: 'Fruta del dragón', price: '$1', stocked: false },
  { id: 3, name: 'Fruta de la pasión', price: '$2', stocked: true },
]

Paso 1: Divide el diseño en un árbol de componentes

Mira el diseño y dibuja cajas alrededor de las piezas. Una buena caja es una cosa que hace un trabajo, el mismo instinto que usas al decidir qué debe hacer una función. Nuestra pantallita se divide claramente en algunas:

  • FilterableProductList envuelve todo.
  • SearchBar contiene la entrada de texto y la casilla de productos en stock.
  • ProductTable muestra la lista de productos.
  • ProductRow es una sola fila en esa tabla.

Estas se anidan dentro de unas otras, así que forman un árbol de componentes:

FilterableProductList
├── SearchBar
└── ProductTable
    └── ProductRow  (uno por producto)

No hay una división única correcta, y la regla práctica del capítulo de componentes aplica aquí: mantén cada uno pequeño y dale un trabajo claro. Una fila sabe cómo dibujar un producto; la tabla sabe cómo distribuir filas; la barra de búsqueda sabe sobre los controles. Si una pieza empieza a hacer dos cosas no relacionadas, eso usualmente es una señal de que la dividas.

Paso 2: Construye una versión estática solo con props

Ahora construye una versión que renderiza los datos pero no hace nada aún. Sin clicks, sin filtrado, sin escribir que cambie nada. Los datos fluyen de una manera, hacia abajo por el árbol, a través de props. Ningún useState aparece en ningún lado en este paso.

jsx
function FilterableProductList({ products }) {
  return (
    <div>
      <SearchBar />
      <ProductTable products={products} />
    </div>
  )
}

function ProductTable({ products }) {
  return (
    <table>
      <tbody>
        {products.map(product => (
          <ProductRow key={product.id} product={product} />
        ))}
      </tbody>
    </table>
  )
}

function ProductRow({ product }) {
  return (
    <tr>
      <td>{product.name}</td>
      <td>{product.price}</td>
    </tr>
  )
}

function SearchBar() {
  return (
    <form>
      <input type="text" placeholder="Buscar..." />
      <label>
        <input type="checkbox" /> Solo mostrar productos en stock
      </label>
    </form>
  )
}

Esto renderiza la lista completa cada vez. La caja de búsqueda y la casilla están en pantalla, pero aún no hacen nada. Ese es el punto del paso: obtienes todo el diseño funcionando solo desde props, y confirmas que el árbol del paso 1 realmente se sostiene, antes de que ninguna de las partes móviles esté involucrada. El array products llega como un prop en la parte superior y se pasa hacia abajo; nada aquí recuerda o cambia nada.

Paso 3: Encuentra el estado mínimo

Ahora hazlo interactivo, y la primera pregunta es cuáles valores necesitan ser estado en absoluto. El estado es costoso de mantener correcto, así que quieres lo menos posible. El resto lo calcularás.

La prueba para cada valor es corta. Haz dos preguntas:

  1. ¿Cambia con el tiempo?
  2. ¿Es algo que no puedas calcular de otro valor que ya tengas?

Si ambas son sí, es estado. Si cualquiera es no, no es. Recorre los valores en nuestra interfaz a través de esa prueba:

  • La lista de productos. Se pasa dentro y no cambia mientras el usuario juega con la página, así que no es estado. Es un prop.
  • El texto de búsqueda que el usuario escribió. Cambia con el tiempo, y no hay nada de lo que calcularlo. Es estado.
  • Que la casilla de productos en stock esté activada. Misma historia: cambia, y nada la deriva. Es estado.
  • La lista filtrada realmente mostrada en pantalla. Cambia, pero puedes calcularla de los productos, el texto de búsqueda y la casilla. Así que no es estado.

Ese último es la regla que vale la pena grabar: deriva, no dupliques. La lista visible son los productos pasados a través de los filtros actuales, así que la calculas durante el render en lugar de almacenar una segunda copia en estado:

jsx
const visible = products.filter(product => {
  const matchesText = product.name
    .toLowerCase()
    .includes(filterText.toLowerCase())
  const matchesStock = !inStockOnly || product.stocked
  return matchesText && matchesStock
})

Si hubieras almacenado visible en su propio useState, tendrías que recordar actualizarlo cada sola vez que los productos, el texto o la casilla cambien. Si te olvidas de uno, la pantalla muestra una lista obsoleta. Derivarlo significa que solo hay una fuente de verdad, y nunca puede quedarse sin sincronizar, porque se recalcula fresco en cada render. Así que el estado mínimo aquí es exactamente dos valores: filterText e inStockOnly.

Paso 4: Decide dónde debe vivir cada pieza de estado

Tienes dos piezas de estado. Ahora averigua cuál componente es dueño de cada una. La regla es poner el estado en el padre común más cercano de cada componente que lo necesite, que es la misma idea que el capítulo de levantar estado explora en profundidad.

Averigua quién necesita cada valor:

  • SearchBar necesita ambos valores, porque renderiza la entrada y la casilla que los muestran.
  • ProductTable también necesita ambos valores, porque filtra las filas usando ellos.

SearchBar y ProductTable son hermanos. Un valor solo puede fluir hacia abajo a través de props, así que ni el hermano puede pasar el estado al otro. El componente más cercano que se sienta encima de ambos es FilterableProductList. Ahí es donde va el estado:

jsx
import { useState } from 'react'

function FilterableProductList({ products }) {
  const [filterText, setFilterText] = useState('')
  const [inStockOnly, setInStockOnly] = useState(false)

  return (
    <div>
      <SearchBar
        filterText={filterText}
        inStockOnly={inStockOnly}
        onFilterTextChange={setFilterText}
        onInStockOnlyChange={setInStockOnly}
      />
      <ProductTable
        products={products}
        filterText={filterText}
        inStockOnly={inStockOnly}
      />
    </div>
  )
}

El estado vive en el padre, y los valores fluyen hacia abajo a ambos hijos como props. ProductTable los lee para construir su lista visible; SearchBar los lee para llenar la entrada y la casilla.

Paso 5: Añade la interacción que actualiza el estado

Los valores llegan a los hijos, pero el usuario aún no puede cambiarlos. Los datos solo fluyen hacia abajo, así que para enviar un cambio hacia arriba pasas los setters hacia abajo como props y dejas que el hijo los llame. FilterableProductList ya pasa onFilterTextChange e onInStockOnlyChange arriba. SearchBar los conecta a las entradas:

jsx
function SearchBar({
  filterText,
  inStockOnly,
  onFilterTextChange,
  onInStockOnlyChange,
}) {
  return (
    <form>
      <input
        type="text"
        placeholder="Buscar..."
        value={filterText}
        onChange={e => onFilterTextChange(e.target.value)}
      />
      <label>
        <input
          type="checkbox"
          checked={inStockOnly}
          onChange={e => onInStockOnlyChange(e.target.checked)}
        />{' '}
        Solo mostrar productos en stock
      </label>
    </form>
  )
}

Ahora el ciclo está cerrado. Escribir en la caja llama onFilterTextChange, que es setFilterText arriba en el padre. Eso actualiza el estado, el padre se re-renderiza, el nuevo filterText fluye de vuelta hacia abajo a ambos hijos, ProductTable recalcula su lista visible, y la pantalla coincide con lo que el usuario escribió. La casilla funciona de la misma manera. El estado baja como un valor, los cambios suben como una llamada a función, y la lista derivada lo sigue por su cuenta.

Ese es el método completo: dibuja el árbol de componentes, constrúyelo estático con props, encuentra el estado mínimo, levántalo al dueño correcto, luego conecta la interacción. Casi cualquier pantalla que encuentres cede ante esos cinco pasos.

El paso 3 es el que recompensa la atención, porque "mínimo" es más estricto de lo que parece a primera vista. La pregunta de trabajo es: ¿cuál es el conjunto más pequeño de valores de los que cada otro valor en pantalla puede ser recomputado? Para nuestra lista eso es filterText e inStockOnly, y todo lo visible es una función pura de esos dos más el products que entra. Cualquier cosa que puedas derivar durante el render, debes hacerlo. La lista filtrada es el caso más claro, pero la misma lógica descarta almacenar un count de coincidencias, un booleano hasResults, o una copia ordenada del array. Cada uno de esos es una función del estado que ya tienes, así que mantenerlo en su propio useState te compra un segundo valor a mantener sincronizado y una nueva forma de estar equivocado. Los valores derivados no pueden quedar obsoletos, porque no persisten; se recalculan en cada render de la única fuente de verdad.

El compromiso a vigilar en el otro lado es derivar algo costoso en cada render. Recurre a useMemo solo cuando el profiling muestra un costo real. Lo predeterminado sigue siendo: deriva durante el render, y promueve un valor a estado solo cuando cambia con el tiempo y nada lo produce.

El paso 1 tiene su propio juicio, y corta en ambas direcciones. Divide demasiado poco y un componente crece en un enredo que es difícil de reutilizar o razonar. Divide demasiado y obtienes un spray de componentes diminutos tejiendo props a través de capas que existen solo para pasarlos, que es su propia clase de difícil de seguir. La prueba útil es responsabilidad y reutilización: un límite se gana su lugar cuando la pieza tiene un trabajo claro, cuando se repite (un ProductRow renderizado una vez por item), o cuando sacarlo hace el padre legible. Un límite que solo renderiza en un lugar y reenvía sus props intactos es usualmente uno que puedes inline. Deja que la duplicación real y la complejidad real separen componentes, en lugar de dividirse en la conjetura de que necesitarás las costuras más tarde.

JunoCinco pasos del diseño a React Aquí está la receta en la que puedes apoyarte: dibuja cajas alrededor de las piezas para obtener tus componentes, constrúyelo con props primero para que muestre los datos y nada más, luego encuentra los pocos valores que realmente cambian y no pueden ser trabajados desde otra cosa, esos son tu estado.

Pon cada uno en el componente más cercano arriba de todo lo que lo necesita, y pasa un setter hacia abajo para que un click o una pulsación de tecla pueda actualizarlo.

La lista que muestras en pantalla no es estado; la calculas de los productos y los filtros cada vez.

JunoCinco pasos del diseño a React El método es el mismo cada vez: árbol de componentes, versión estática solo con props, estado mínimo, levántalo al padre común más cercano, conecta la interacción.

La pregunta de estado es la aguda, si un valor cambia con el tiempo y no puedes derivarlo, es estado, de otra forma calcúlalo durante el render.

Deriva, no dupliques: almacena filterText e inStockOnly, y calcula la lista visible de ellos para que nunca pueda quedarse sin sincronizar.

JunoCinco pasos del diseño a React Estado mínimo significa el conjunto más pequeño del que todo lo demás es una función pura; aquí, filterText e inStockOnly, con la lista filtrada, conteos y banderas todas derivadas durante el render en lugar de almacenadas. Estado extra es otra forma de desincronizarse, y useMemo se gana su lugar solo una vez que el profiling muestra un costo real.

Los límites de componentes son el otro juicio: divide por un trabajo real, repetición o legibilidad, e inline los wrappers pass-through que añadiste en la especificación.

Lo siguiente: React Accesible, donde la interfaz que acabas de diseñar se vuelve usable por todos.