Luchando con Safari: React Keys para Forzar Repaint
En un proyecto, descubrí junto a un gran dev un bug peculiar que solo ocurría en Safari: el total del carrito no se actualizaba visualmente en el botón de pago, aunque en consola aparecía correcto.
Lo más extraño: al pasar el mouse sobre el botón o hacer scroll, mágicamente se actualizaba. Además era un comportamiento intermitente en algunos casos. Fue un momento bastante gracioso porque nos mirábamos sin entender qué estaba pasando. 😅
El Problema
// console.log(totalAmount) → "S/ 150.00" ✅
// Pero el botón muestra → "S/ 100.00" ❌
// Hover → Ahora sí muestra "S/ 150.00" 🤷♂️
Este comportamiento nos desconcertó por completo. Era claramente un problema de renderizado, no de lógica, ya que los datos estaban correctos en el estado de React.
Qué Dedujimos
Después de revisar y probar diferentes cosas, llegamos a la idea de que Safari tiene un problema de optimización con elementos que tienen position: sticky y se encuentra cerca al borde o parcialmente fuera del viewport.
Cuando solo cambia el contenido textual del elemento, Safari no repinta automáticamente el componente, esperando alguna interacción del usuario para hacerlo.
Otros navegadores como Chrome y Firefox sí lo hacen automáticamente, por lo que el bug era exclusivo de Safari.
La Solución
La vista base estaba construida usando un componente clase de React y contenía mucha lógica.
La solución que planteamos fue un parche para el hotfix: usar una key dinámica basada en el total.
Cuando React ve una key diferente, recrea completamente el componente, y Safari sí repinta elementos nuevos.
Esto lo basamos en que key es una propiedad importante de React para repintar y es comúnmente usada en listas, pero para este caso la usamos como alternativa para forzar el repintado.
export const Footer = ({
totalAmount = '' // el monto formateado
}: IFooter) => {
const numericKey = totalAmount.replace(/[^0-9.]/g, '')
return (
<Container>
<MainButton key={`finish-${numericKey}`} onClick={onCreate}>
Crear y finalizar <strong>{totalAmount}</strong>
</MainButton>
</Container>
)
}
Cómo Funciona
El truco está en extraer el valor numérico del total formateado y usarlo como parte de la key:
// Render 1: Total S/ 100.00
<MainButton key="finish-100.00">S/ 100.00</MainButton>
// Render 2: Total S/ 150.00
<MainButton key="finish-150.00">S/ 150.00</MainButton>
// ↑ Key diferente → React recrea el componente
// → Safari lo ve como "nuevo" → Lo repinta ✅
Cuando cambia el total, cambia la key, React desmonta el componente anterior y monta uno nuevo. Safari trata este nuevo componente como un elemento completamente nuevo y lo repinta correctamente.
Resultados
La solución nos permitió atender una incidencia crítica para el cliente (Si es buena práctica o no, fue lo menos importante en ese momento):
- ✅ Safari actualiza correctamente sin interacción del usuario
- ✅ Comportamiento consistente en todos los navegadores
- ✅ Parche simple y mantenible
- ✅ Sin impacto significativo en el rendimiento
Cuándo Aplicar Este Patrón
Usa este patrón cuando observes:
- Valor correcto en consola pero incorrecto en UI
- Hover o scroll lo arregla (síntoma clave de problema de repaint)
- Elemento con
position: stickyoposition: fixed - Solo ocurre en Safari (o algún navegador específico)
Conclusión
Los bugs específicos de navegadores pueden ser frustrantes, pero entender cómo funciona el ciclo de vida de React y los mecanismos de repaint de los navegadores te da herramientas poderosas para resolverlos.
Lee con cuidado este artículo y llévate otro caso de uso de la key de React. Una propiedad que usualmente manejamos en listas pero aquí es un caso diferente y real.