← Regresar a lecciones
  • swift

  • kotlin

  • zustand

  • react native

  • mobile

  • React Navigation

  • state-management

Zustand: Estado Global en React Native Sin el Drama

¿Qué problema resuelve Zustand?
  • Instalación

Si vienes de desarrollo nativo mobile (Kotlin, Swift), sabes que elegir tu herramienta de estado es crucial. En Android tienes ViewModel + StateFlow, en iOS SwiftUI con @StateObject. En React Native, Zustand se ha convertido en la opción favorita por una razón simple: funciona y no te complica la vida.

¿Qué problema resuelve Zustand?

Imagina que estás construyendo una app de e-commerce. Necesitas:

  • Saber quién está logueado (usuario)
  • Qué productos tiene en el carrito
  • Sus preferencias (tema claro/oscuro, idioma)

Este estado necesita estar accesible desde múltiples pantallas. La pregunta es: ¿cómo lo compartes?

Context API es la solución nativa de React, pero tiene un problema grave, cada vez que algo cambia en el contexto, todos los componentes que lo usan se re-renderizan. Incluso si solo necesitan una pequeña parte del estado. Zustand resuelve esto con selectores inteligentes.

Instalación

1npm install zustand

Solo 1KB. Menos que un componente de UI promedio.

Tu primer store

Si vienes de Kotlin o Swift, un store es similar a un ViewModel (Android) o un ObservableObject (SwiftUI). Es una clase que mantiene el estado de tu aplicación y expone métodos para modificarlo. La diferencia clave: En vez de crear una clase, usas una función que retorna un objeto con tu estado y métodos.

1// store/useStore.js 2import { create } from 'zustand'; 3 4export const useStore = create((set) => ({ 5 // Estado inicial - como las propiedades de tu ViewModel/ObservableObject 6 user: null, 7 cart: [], 8 9 // Acciones - como los métodos públicos de tu ViewModel 10 login: (user) => set({ user }), 11 12 logout: () => set({ user: null, cart: [] }), 13 14 addToCart: (product) => set((state) => ({ 15 cart: [...state.cart, product] 16 })), 17 18 removeFromCart: (productId) => set((state) => ({ 19 cart: state.cart.filter(item => item.id !== productId) 20 })) 21}));

La ventaja de Zustand es que el componente solo se re-renderiza cuando cart cambia. Si user cambia, este componente no se entera. En ViewModel y ObservableObject tendrías que gestionar esto manualmente con selectores o computed properties.

Usando el store en componentes

1// screens/CartScreen.js 2export default function CartScreen() { 3 const cart = useStore((state) => state.cart); 4 const removeFromCart = useStore((state) => state.removeFromCart); 5 6 return ( 7 <View> 8 <Text>Carrito ({cart.length} items)</Text> 9 <FlatList 10 data={cart} 11 keyExtractor={(item) => item.id} 12 renderItem={({ item }) => ( 13 <View> 14 <Text>{item.name}</Text> 15 <TouchableOpacity onPress={() => removeFromCart(item.id)}> 16 <Text>Eliminar</Text> 17 </TouchableOpacity> 18 </View> 19 )} 20 /> 21 </View> 22 ); 23}
1// components/UserProfile.js 2export default function UserProfile() { 3 const user = useStore((state) => state.user); 4 5 if (!user) return null; 6 7 return ( 8 <View> 9 <Text>{user.name}</Text> 10 <Text>{user.email}</Text> 11 </View> 12 ); 13}

Nota algo importante: UserProfile solo se re-renderiza cuando user cambia. Los cambios en cart no lo afectan. Esto es rendimiento automático.

La magia de los selectores

Un selector es una función que extrae solo la parte del estado que necesitas:

1// Solo se actualiza cuando cart.length cambia 2const cartCount = useStore((state) => state.cart.length); 3 4// Solo cuando el nombre del usuario cambia 5const userName = useStore((state) => state.user?.name); 6 7// Selector computado: calcula el total 8const total = useStore((state) => 9 state.cart.reduce((sum, item) => sum + item.price, 0) 10);

Cada selector es una suscripción independiente. Zustand compara el resultado anterior con el nuevo, y solo actualiza el componente si cambió.

El problema de la comparación superficial

JavaScript compara objetos por referencia, no por contenido. Esto causa un problema común:

1// ❌ MALO: Se re-renderiza siempre 2function CartSummary() { 3 const { cart, user } = useStore((state) => ({ 4 cart: state.cart, 5 user: state.user 6 })); 7 // Cada vez retorna un nuevo objeto { cart, user } 8}

La solución es usar shallow para comparar las propiedades internas:

1// ✅ BUENO: Solo se actualiza si cart o user cambian 2function CartSummary() { 3 const { cart, user } = useStore( 4 (state) => ({ 5 cart: state.cart, 6 user: state.user 7 }), 8 shallow 9 ); 10 11 const total = cart.reduce((sum, item) => sum + item.price, 0); 12 13 return ( 14 <View> 15 <Text>{user?.name}</Text> 16 <Text>{cart.length} items - ${total}</Text> 17 </View> 18 ); 19}

Acciones con lógica compleja

Puedes acceder al estado actual dentro de las acciones usando get:

1export const useStore = create((set, get) => ({ 2 products: [], 3 cart: [], 4 5 addToCart: (productId) => { 6 const state = get(); 7 const product = state.products.find(p => p.id === productId); 8 9 if (!product) { 10 console.error('Producto no encontrado'); 11 return; 12 } 13 14 const existsInCart = state.cart.some(item => item.id === productId); 15 16 if (existsInCart) { 17 // Incrementar cantidad 18 set((state) => ({ 19 cart: state.cart.map(item => 20 item.id === productId 21 ? { ...item, quantity: item.quantity + 1 } 22 : item 23 ) 24 })); 25 } else { 26 // Agregar nuevo 27 set((state) => ({ 28 cart: [...state.cart, { ...product, quantity: 1 }] 29 })); 30 } 31 } 32}));

Integración con React Navigation

Zustand funciona perfecto con React Navigation sin configuración especial:

1// navigation/AppNavigator.js 2export default function AppNavigator() { 3 const isAuthenticated = useStore((state) => state.user !== null); 4 5 return ( 6 <NavigationContainer> 7 <Stack.Navigator> 8 {isAuthenticated ? ( 9 <> 10 <Stack.Screen name="Home" component={HomeScreen} /> 11 <Stack.Screen name="Cart" component={CartScreen} /> 12 <Stack.Screen name="Profile" component={ProfileScreen} /> 13 </> 14 ) : ( 15 <> 16 <Stack.Screen name="Login" component={LoginScreen} /> 17 <Stack.Screen name="Register" component={RegisterScreen} /> 18 </> 19 )} 20 </Stack.Navigator> 21 </NavigationContainer> 22 ); 23}

Si necesitas navegar desde una acción del store, pasa la referencia de navigation:

1export const useStore = create((set) => ({ 2 user: null, 3 4 loginAndNavigate: async (credentials, navigation) => { 5 try { 6 const response = await fetch('/api/login', { 7 method: 'POST', 8 body: JSON.stringify(credentials) 9 }); 10 const user = await response.json(); 11 12 set({ user }); 13 navigation.replace('Home'); 14 } catch (error) { 15 console.error('Login failed:', error); 16 } 17 } 18})); 19 20// En tu LoginScreen 21function LoginScreen({ navigation }) { 22 const loginAndNavigate = useStore((state) => state.loginAndNavigate); 23 24 const handleLogin = () => { 25 loginAndNavigate({ email, password }, navigation); 26 }; 27}

Persistencia con AsyncStorage

Para guardar el estado entre sesiones de la app:

1npm install async-storage
1export const useStore = create( 2 persist( 3 (set, get) => ({ 4 user: null, 5 theme: 'light', 6 cart: [], 7 8 login: (user) => set({ user }), 9 toggleTheme: () => set((state) => ({ 10 theme: state.theme === 'light' ? 'dark' : 'light' 11 })) 12 }), 13 { 14 name: 'app-storage', 15 storage: createJSONStorage(() => AsyncStorage), 16 17 // Solo persiste algunas propiedades 18 partialize: (state) => ({ 19 user: state.user, 20 theme: state.theme 21 // cart NO se persiste (es temporal) 22 }) 23 } 24 ) 25);

Ahora user y theme se guardan automáticamente. Cuando el usuario abre la app de nuevo, el estado se restaura.

Organización para apps grandes

Para apps pequeñas o medianas, un solo store funciona perfecto. Pero si tu store pasa de 300-400 líneas, puedes dividirlo en slices:

1// store/slices/authSlice.js 2export const createAuthSlice = (set) => ({ 3 user: null, 4 isAuthenticated: false, 5 6 login: (user) => set({ user, isAuthenticated: true }), 7 logout: () => set({ user: null, isAuthenticated: false }) 8}); 9 10// store/slices/cartSlice.js 11export const createCartSlice = (set) => ({ 12 cart: [], 13 14 addToCart: (product) => set((state) => ({ 15 cart: [...state.cart, product] 16 })), 17 18 clearCart: () => set({ cart: [] }) 19}); 20 21// store/useStore.js 22export const useStore = create((...args) => ({ 23 ...createAuthSlice(...args), 24 ...createCartSlice(...args) 25}));

Por qué Zustand funciona en React Native

React Native tiene requisitos de rendimiento estrictos, asi que necesitas mantener 60fps constantes o la app se siente lenta.

Zustand ayuda con esto por las actualizaciones granulares, es decir, solo los componentes suscritos a una parte específica del estado se actualizan, no tiene re-renders en cascada, tiene mínimo overhead (1KB de código es prácticamente nada) y cuenta con optimización automática.

Conclusión

Zustand te da estado global sin el drama de Context API o el boilerplate de Redux. Es especialmente bueno para React Native donde el rendimiento importa.

Cuándo usar Zustand:

  • Estado compartido entre múltiples pantallas
  • Datos de usuario y autenticación
  • Carritos, favoritos, configuraciones
  • Cualquier dato que necesites en varios lugares

Cuándo NO usar Zustand:

  • Estado local de un solo componente (usa useState)
  • Estado de formularios (usa react-hook-form)
  • Estado de servidor (usa react-query o SWR)