swift
kotlin
zustand
react native
mobile
React Navigation
state-management
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.
Imagina que estás construyendo una app de e-commerce. Necesitas:
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.
1npm install zustand
Solo 1KB. Menos que un componente de UI promedio.
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.
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.
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ó.
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}
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}));
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}
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}
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.
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}));
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.
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:
Cuándo NO usar Zustand: