Props, State y Re-renderizados en Móvil
Cómo se propagan las actualizaciones en hilos nativos frente al tiempo de ejecución de JS.
Busca en todas las páginas de la documentación
Cómo se propagan las actualizaciones en hilos nativos frente al tiempo de ejecución de JS.
Tarjeta de referencia rápida - lista para copiar y pegar.
import { useCallback, useState } from "react";
import { FlatList, Pressable, StyleSheet, Text, View } from "react-native";
interface CounterProps {
label: string;
initialCount?: number;
}
export function Counter({ label, initialCount = 0 }: CounterProps) {
const [count, setCount] = useState(initialCount);
const increment = useCallback(() => {
setCount((prev) => prev + 1);
}, []);
return (
<View style={styles.row}>
<Text style={styles.label}>{label}</Text>
<Text style={styles.value}>{count}</Text>
<Pressable onPress={increment} style={styles.button}>
<Text style={styles.buttonText}>+1</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
row: { flexDirection: "row", alignItems: "center", gap: 12, padding: 12 },
label: { flex: 1, fontSize: 16 },
value: { fontSize: 18, fontWeight: "700", minWidth: 32, textAlign: "right" },
button: { backgroundColor: "#2563eb", paddingHorizontal: 14, paddingVertical: 8, borderRadius: 8 },
buttonText: { color: "#fff", fontWeight: "600" },
});Cuándo usarlo: Cualquier pantalla interactiva donde las props del padre configuren hijos y useState local maneje la interfaz que debe mantenerse sincronizada con vistas nativas.
import { memo, useCallback, useMemo, useState } from "react";
import {
ActivityIndicator,
FlatList,
Pressable,
StyleSheet,
Text,
View,
} from "react-native";
interface Todo {
id: string;
title: string;
done: boolean;
}
interface TodoRowProps {
item: Todo;
onToggle: (id: string) => void;
}
const TodoRow = memo(function TodoRow({ item, onToggle }: TodoRowProps) {
return (
<Pressable
onPress={() => onToggle(item.id)}
style={[styles.row, item.done && styles.rowDone]}
>
<Text style={[styles.title, item.done && styles.titleDone]}>{item.title}</Text>
<Text style={styles.badge}>{item.done ? "Hecho" : "Abierto"}</Text>
</Pressable>
);
});
const SEED: Todo[] = [
{ id: "1", title: "Disposición de perfil con View + Text", done: true },
{ id: "2", title: "Conectar manejadores Pressable", done: false },
{ id: "3", title: "Medir rendimiento de desplazamiento en lista", done: false },
];
export default function TodoScreen() {
const [todos, setTodos] = useState<Todo[]>(SEED);
const [filter, setFilter] = useState<"all" | "open">("all");
const [loading, setLoading] = useState(false);
const visible = useMemo(
() => (filter === "open" ? todos.filter((t) => !t.done) : todos),
[todos, filter]
);
const openCount = useMemo(() => todos.filter((t) => !t.done).length, [todos]);
const handleToggle = useCallback((id: string) => {
setTodos((prev) =>
prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t))
);
}, []);
const handleRefresh = useCallback(async () => {
setLoading(true);
await new Promise((r) => setTimeout(r, 400));
setTodos(SEED);
setLoading(false);
}, []);
return (
<View style={styles.screen}>
<View style={styles.header}>
<Text style={styles.heading}>Tareas ({openCount} abiertos)</Text>
<View style={styles.filters}>
{(["all", "open"] as const).map((key) => (
<Pressable
key={key}
onPress={() => setFilter(key)}
style={[styles.chip, filter === key && styles.chipActive]}
>
<Text style={[styles.chipText, filter === key && styles.chipTextActive]}>
{key === "all" ? "Todos" : "Abiertos"}
</Text>
</Pressable>
))}
</View>
</View>
{loading ? (
<ActivityIndicator style={styles.loader} />
) : (
<FlatList
data={visible}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <TodoRow item={item} onToggle={handleToggle} />}
ItemSeparatorComponent={() => <View style={styles.separator} />}
contentContainerStyle={styles.list}
/>
)}
<Pressable onPress={handleRefresh} style={styles.refresh}>
<Text style={styles.refreshText}>Restablecer lista</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, backgroundColor: "#f8fafc" },
header: { padding: 16, gap: 12 },
heading: { fontSize: 22, fontWeight: "700", color: "#0f172a" },
filters: { flexDirection: "row", gap: 8 },
chip: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 999, backgroundColor: "#e2e8f0" },
chipActive: { backgroundColor: "#2563eb" },
chipText: { fontSize: 14, fontWeight: "600", color: "#334155" },
chipTextActive: { color: "#fff" },
list: { paddingHorizontal: 16, paddingBottom: 24 },
row: {
flexDirection: "row",
alignItems: "center",
backgroundColor: "#fff",
padding: 14,
borderRadius: 10,
gap: 8,
},
rowDone: { opacity: 0.7 },
title: { flex: 1, fontSize: 16, color: "#0f172a" },
titleDone: { textDecorationLine: "line-through", color: "#64748b" },
badge: { fontSize: 12, fontWeight: "600", color: "#2563eb" },
separator: { height: 8 },
loader: { marginTop: 32 },
refresh: { margin: 16, padding: 14, borderRadius: 10, backgroundColor: "#0f172a" },
refreshText: { color: "#fff", textAlign: "center", fontWeight: "600" },
});Lo que esto demuestra:
item, onToggle) fluyen hacia una fila memoizada mientras el estado (todos, filter) vive en la pantallasetState funcionales evitan cierres obsoletos al activar elementosuseMemo obtiene datos filtrados y cuenta abiertos sin re-renderizados extras de solo recompilaciónuseCallback mantiene handleToggle estable para que memo en TodoRow pueda saltar filas sin cambiosFlatList re-renderiza solo filas visibles - el estado padre aún dispara reconciliación para esas filasuseState y useReducer programan un re-renderizado cuando se ejecuta el actualizador; el valor devuelto es la instantánea del renderizado actual.setState en manejadores de eventos, promesas y tiempos de espera en un renderizado.Toque del usuario (hilo de UI)
- evento enviado al hilo de JS
- onPress / manejador se ejecuta
- setState programa renderizado
- función del componente se vuelve a ejecutar (JS)
- React compara árbol virtual (JS)
- Fabric confirma nodos de sombra cambiados
- disposición nativa + pintura (hilo de UI)
| Patrón | API | Nota móvil |
|---|---|---|
| Estado de interfaz local | useState | Campos de formulario, alternancias, estado abierto de hojas |
| Datos derivados | useMemo | Listas filtradas - más barato que almacenar estado duplicado |
| Manejadores estables | useCallback | Requerido para optimización de fila memo/FlatList |
| Referencias mutables | useRef | Temporizadores, manijas de animación - sin re-renderizado en cambio de .current |
| Estado de pantalla compartida | Context / store | Prefiere estado colocalizado hasta que múltiples pestañas lo necesiten |
| Datos del servidor | TanStack Query / use | Mantén el fetching fuera del renderizado; muestra esqueletos mientras está pendiente |
| Situación | Usa props | Usa state |
|---|---|---|
| Valor propiedad del padre | ✓ | |
| Valor cambia solo dentro del componente | ✓ | |
| Componentes hermanos necesitan el mismo valor | Elevar al padre ✓ | |
| Valor de parámetros de navegación | Prop de ruta ✓ | Copiar a state si es editable |
| Carga útil de API remota | Pasar después de fetch | Almacenar en caché de consulta |
import type { Dispatch, SetStateAction } from "react";
interface ScreenProps {
userId: string;
onSignOut: () => void;
}
// Tipo de definidor explícito para callbacks que reenvían setState
type SetTodos = Dispatch<SetStateAction<Todo[]>>;
// Uniones discriminadas para interfaz asincrónica - evita estados imposibles de carga + error
type LoadState =
| { status: "idle" }
| { status: "loading" }
| { status: "error"; message: string }
| { status: "success"; data: Todo[] };interface o type; exporta props de fila cuando uses memo.SetStateAction<T> cubre formas de valor y función de actualizador.Callbacks en línea inestables en listas - onPress={() => toggle(item.id)} dentro de renderItem rompe memo en cada renderizado. Solución: Pasa id a una fila memoizada que llama un manejador de useCallback estable.
Almacenar datos derivados en estado - Mantener openTodos en estado junto a todos duplica actualizaciones y riesgo de divergencia. Solución: Obtén con useMemo de la fuente de verdad.
Mutar objetos de estado - todo.done = true luego setTodos(todos) puede saltar el re-renderizado porque la referencia no cambió. Solución: Devuelve nuevos objetos/arrays de actualizadores.
Asumir pintura nativa inmediata después de setState - JS termina el paso de renderizado antes de que native confirme; las ráfagas rápidas aún ponen en cola trabajo. Solución: Agrupa actualizaciones relacionadas; evita lecturas de disposición sincrónica después de cada tick.
Valor de Context recreado en cada renderizado - value={{ user, theme }} re-renderiza todos los consumidores. Solución: Memoriza el objeto de valor de context o divide contexts.
Sobre-memorizar todo - useMemo/useCallback tienen su propio costo y ruido. Solución: Perfila primero; optimiza filas de listas y cálculos puros pesados.
Leer estado justo después de setState - setCount(1); console.log(count) aún registra la instantánea antigua. Solución: Usa la forma de actualizador o useEffect clavado en el valor.
| Alternativa | Usa cuando | No uses cuando |
|---|---|---|
useState local | Interfaz de pantalla con alcance que no cruza rutas | Muchos primos distantes necesitan los mismos datos |
| React Context | Tema, sesión de auth, feature flags leídas ampliamente | Actualizaciones de alta frecuencia (posición de desplazamiento, pulsaciones de tecla) |
| Zustand / Jotai | Estado de aplicación mediano con boilerplate mínimo | Un callback padre-hijo es suficiente |
| TanStack Query | Listas de servidor, almacenamiento en caché, actualización de fondo | Toggle puramente local sin red |
useRef para valores mutables | Fotogramas de animación, intervalos, manijas imperativas | Valores que deben aparecer en la pantalla |
| Entradas no controladas + ref | Formularios simples con pocos campos | Validación compleja entre pasos |
memo).setTimeout, resoluciones de fetch y eventos nativos se agrupan por defecto.setState en el mismo tick producen un renderizado.flushSync solo cuando intencionalmente necesites una pintura sincrónica (raro en móvil).renderItem a menos que sea memoizado; la nueva identidad de función puede derrotar la memoización de fila.memo en la fila, manejadores estables de useCallback y extraData solo cuando sea necesario.setState / useReducer.style={{ flex: 1 }} en línea crea un nuevo objeto cada vez.useCallback y eleva estilos a StyleSheet.create.useRef almacena valores mutables que no deben disparar re-renderizados cuando se actualicen.useState.FlatList es un PureComponent - puede saltar renderItem si la referencia de data no cambió.extraData={selectedId} cuando la apariencia de fila depende de estado fuera de data.// Incorrecto - cada llamada usa la misma instantánea
setCount(count + 1);
setCount(count + 1);
// Correcto - actualizador funcional se encadena en el valor más reciente
setCount((c) => c + 1);
setCount((c) => c + 1);count en el cierre es obsoleta dentro del mismo manejador.memo rechace con las mismas props.children cambia si el padre define en línea JSX que captura nuevos cierres.expo-router y React Navigation pasan parámetros de ruta como props o valores de hook (useLocalSearchParams).useEffect, no al cuerpo de renderizado.setTodos((prev) => ...) para no cerrar sobre todos.id en lugar de capturar variables de bucle cuando sea posible.useMemo solo cachea un valor computado entre renderizados del mismo componente.memo en hijos cuando pases objetos o arrays memoizados hacia abajo.Versiones de Stack: Esta página fue escrita para React 19.2.3, React Native 0.86.0, y Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última actualización: 16 jul 2026