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

Pensando en React

Cada capítulo hasta ahora te enseñó una pieza: componentes, props, estado, eventos, listas. Este capítulo es donde todo se une 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 unas cuantas veces, recurrirás a los mismos cinco pasos en casi todas las pantallas que construyas.

Vamos a recorrer una pequeña UI de principio a fin: una lista de productos buscable. Muestra un conjunto de productos, una caja de búsqueda para filtrarlos por nombre, y una casilla para ocultar lo 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: 'Maracuyá', price: '$2', stocked: true },
]

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

Mira el diseño y dibuja recuadros alrededor de las piezas. Un buen recuadro es algo que hace un trabajo, el mismo instinto que usas cuando decides qué debe hacer una función. Nuestra pequeña pantalla se divide limpiamente en algunos:

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

Estos se anidan uno dentro del otro, así que forman un árbol de componentes:

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

No hay una única división correcta, y la regla de oro del capítulo 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 de los controles. Si una pieza empieza a hacer dos cosas no relacionadas, eso es usualmente una señal de dividirla.

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

Ahora construye una versión que renderiza los datos pero no hace nada más. Sin clicks, sin filtrado, sin escribir que cambie algo. Los datos fluyen de una sola forma, hacia abajo del árbol, a través de props. No hay useState 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 no hacen nada aún. Ese es el punto del paso: obtienes todo el layout funcionando solo con props, y confirmas que el árbol del paso 1 realmente se mantiene unido, antes de que ninguna de las partes móviles esté involucrada. El array products viene como una 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. Pregúntate dos cosas:

  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 alguna es no, no lo es. Pasa los valores en nuestra UI a través de esa prueba:

  • La lista de productos. Se pasa y no cambia mientras el usuario interactúa con la página, así que no es estado. Es una prop.
  • El texto de búsqueda que escribió el usuario. Cambia con el tiempo, y no hay nada de lo cual calcularlo. Es estado.
  • Si la casilla de productos en stock está marcada. Misma historia: cambia, y nada lo deriva. Es estado.
  • La lista filtrada realmente mostrada en pantalla. Cambia, pero puedes calcularla a partir 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 es los productos pasados a través de los filtros actuales, así que la calculas durante el renderizado 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 acordarte de actualizarlo cada vez que los productos, el texto o la casilla cambien. Pierdes uno y la pantalla muestra una lista obsoleta. Derivarla significa que hay una sola fuente de verdad, y nunca puede desincronizarse, porque se recalcula fresco en cada renderizado. 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 qué 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 necesita, que es la misma idea que el capítulo elevando el estado explora en profundidad.

Averigua quién necesita cada valor:

  • SearchBar necesita ambos valores, porque renderiza el input y la casilla que los muestran.
  • ProductTable también necesita ambos valores, porque filtra las filas usándolos.

SearchBar y ProductTable son hermanos. Un valor solo puede fluir hacia abajo a través de props, así que ningún hermano puede pasar estado al otro. El componente más cercano que está arriba de ambos es FilterableProductList. Ese es dónde 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 el input y la casilla.

Paso 5: Agrega 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 los inputs:

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 hacia atrás a ambos hijos, ProductTable recalcula su lista visible, y la pantalla coincide con lo que escribió el usuario. La casilla funciona de la misma forma. El estado baja como un valor, los cambios suben como una llamada a función, y la lista derivada la sigue por su cuenta.

Ese es todo el método: dibuja el árbol de componentes, construyelo estático con props, encuentra el estado mínimo, elévalo al dueño correcto, luego conecta la interacción. Casi cualquier pantalla que encuentres cede a 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 del cual cada otro valor en pantalla puede ser recalculado? Para nuestra lista eso es filterText e inStockOnly, y todo lo visible es una función pura de esos dos más el products entrante. Cualquier cosa que puedas derivar durante el renderizado, deberías. La lista filtrada es el caso más claro, pero la misma lógica descarta almacenar un count de coincidencias, un boolean hasResults, o una copia ordenada del array. Cada uno de esos es una función de estado que ya tienes, así que mantenerlo en su propio useState te compra un segundo valor a mantener en sincronía y una nueva forma de estar equivocado. Los valores derivados no pueden volverse obsoletos, porque no persisten; se recalculan en cada renderizado de la una fuente de verdad.

El trade-off a vigilar en el otro lado es derivar algo costoso en cada renderizado. Recurre a useMemo solo cuando profiling muestra un costo real. El default se mantiene: deriva durante el renderizado, y promueve un valor a estado solo cuando cambia con el tiempo y nada lo produce.

El paso 1 tiene su propio juicio de valor, y corta en ambos sentidos. Divide demasiado poco y un componente crece en un enredo que es difícil de reutilizar o razonar. Divide demasiado y obtienes un rocío de componentes diminutos pasando 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: una frontera se gana su lugar cuando la pieza tiene un trabajo claro, cuando se repite (un ProductRow renderizado una vez por item), o cuando sacarla hace el padre legible. Una frontera que solo renderiza en un lugar y reenvía sus props sin tocar es usualmente una que puedes hacer inline. Deja que la duplicación real y la complejidad real dividan componentes, en lugar de dividir en la suposición de que necesitarás las costuras después.

JunoCinco pasos del diseño a React Aquí está la receta en la que puedes apoyarte: dibuja recuadros alrededor de las piezas para obtener tus componentes, construyelo con props primero para que muestre los datos y nada más, luego encuentra los pocos valores que realmente cambian y no pueden ser deducidos de algo más, 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 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, elévalo al padre común más cercano, conecta la interacción. La pregunta de estado es la afilada, si un valor cambia con el tiempo y no puedes derivarlo, es estado, de lo contrario calcúlalo durante el renderizado. Deriva, no dupliques: almacena filterText e inStockOnly, y calcula la lista visible a partir de ellos para que nunca pueda desincronizarse.
JunoCinco pasos del diseño a React El estado mínimo significa el conjunto más pequeño del cual todo lo demás es una función pura; aquí, filterText e inStockOnly, con la lista filtrada, conteos y flags todos derivados durante el renderizado en lugar de almacenados. El estado extra es otra forma de desincronización, y useMemo se gana su lugar solo una vez que profiling muestra un costo real. Las fronteras de componentes son el otro juicio de valor: divide por un trabajo real, repetición o legibilidad, e inserta los wrappers de paso que agregaste en especificación.

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