React
Estado derivado y datos redundantes
Explica cuándo calcular valores durante el render en lugar de guardarlos como estado, reduciendo duplicación, sincronización manual y efectos innecesarios.
- Última actualización
- Actualizada
- Nivel
- Fundamentos
React
Explica cuándo calcular valores durante el render en lugar de guardarlos como estado, reduciendo duplicación, sincronización manual y efectos innecesarios.
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const fullName = `${firstName} ${lastName}`.trim();Guardar fullName como estado crea dos fuentes de verdad y obliga a sincronizarlas.
const visibleProducts = products.filter((product) =>
product.name.toLowerCase().includes(query.toLowerCase()),
);No necesitas un effect que copie el resultado a otro estado.
Un dato merece estado cuando:
function Editor({ initialText }: { initialText: string }) {
const [text, setText] = useState(initialText);
}El prefijo initial comunica que solo se usa para inicializar. Si debe seguir cambios externos, diseña un componente controlado o cambia su key.
useMemo puede evitar un cálculo costoso, pero el valor sigue siendo derivado y puede recalcularse.
isEmpty, total, filteredItems o fullName por separado.Menos estado significa menos combinaciones inválidas. Primero intenta calcular durante render.
Antes de crear un useState, pregunta si el valor puede obtenerse usando props, state existente o constantes.
¿puede calcularse con datos actuales?
├─ sí → derivar durante render
└─ no → evaluar si necesita stateEjemplos normalmente derivados:
fullName desde nombre y apellido.total desde items.isEmpty desde items.length.selectedItem desde ID y colección.filteredItems desde items y query.isValid desde los campos actuales.const [items, setItems] = useState<Item[]>([]);
const [total, setTotal] = useState(0);Cada cambio en items exige recordar actualizar total. Si una transición olvida hacerlo, la UI entra en un estado imposible.
const total = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0,
);Ahora existe una sola autoridad.
La mayoría de cálculos de interfaz son baratos. Filtrar decenas o cientos de elementos suele ser preferible a mantener una copia sincronizada.
Cuando el cálculo es realmente costoso:
useMemo como optimización.const visibleProducts = useMemo(
() => expensiveFilter(products, query),
[products, query],
);Sigue siendo un valor derivado; la memoización no lo convierte en estado independiente.
Evita guardar una copia del objeto:
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);Si la colección recibe datos nuevos, la copia puede quedar obsoleta. Guarda el identificador:
const [selectedId, setSelectedId] = useState<string | null>(null);
const selectedProduct = products.find((product) => product.id === selectedId) ?? null;const [isEmpty, setIsEmpty] = useState(true);Es redundante si depende únicamente de la colección:
const isEmpty = items.length === 0;También evita guardar simultáneamente varios booleanos de una misma máquina:
const [isLoading, setIsLoading] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);Modela un estado exclusivo:
type Status = "idle" | "loading" | "success" | "error";
const [status, setStatus] = useState<Status>("idle");Copiar una prop puede ser correcto cuando nace un valor editable independiente:
function ProfileForm({ initialUser }: { initialUser: User }) {
const [draft, setDraft] = useState(() => ({ ...initialUser }));
}Aquí draft no pretende ser siempre igual a initialUser; representa cambios todavía no confirmados. Debes definir qué ocurre cuando llega otra entidad:
La diferencia semántica justifica el estado.
function Checkout({ cart, discount }: Props) {
const subtotal = cart.items.reduce(calculateSubtotal, 0);
const discountAmount = subtotal * discount.rate;
const total = subtotal - discountAmount;
// ...
}Puedes crear una cadena de cálculos puros durante render. Evita convertir cada paso en un effect y un estado separado.
useEffect(() => {
setVisibleProducts(filterProducts(products, query));
}, [products, query]);Problemas:
Calcula directamente.
A veces se guarda un resultado “para no recalcular”. Eso mezcla semántica con optimización. State representa información mutable del producto; una caché de cálculo pertenece a memoización o a una capa especializada.
No uses state para cachear porque exige decidir cuándo invalidarlo manualmente.
En un store normalizado puedes guardar entidades y relaciones mínimas:
type State = {
usersById: Record<string, User>;
selectedUserId: string | null;
};El usuario seleccionado se deriva. Esto evita duplicar entidades completas en varias ramas.
Señales:
filtered, computed, formatted o isEmpty.Preguntas:
Puede existir state separado cuando:
La clave es que tenga semántica propia y pueda divergir intencionalmente.
useMemo optimiza; no crea una nueva fuente de verdad.selectedProduct puede quedar obsoleto si también guardas selectedId?useReducer organiza transiciones cuando el estado mínimo sigue teniendo reglas coordinadas.