Gestión de Estado en React Native 2026: Zustand, Jotai y TanStack Query

Guía práctica sobre gestión de estado en React Native 2026. Aprende a usar Zustand, Jotai y TanStack Query con la arquitectura híbrida moderna: estado del servidor, estado del cliente y persistencia con MMKV.

Introducción: El Estado Ya No Se Gestiona Como Antes

Si llevas un tiempo trabajando con React Native, seguro recuerdas esa época en la que Redux era la única opción seria para gestionar estado. Configurar un store, definir actions, escribir reducers, conectar componentes con mapStateToProps... Honestamente, era una cantidad absurda de código para tareas relativamente simples. Bueno, en 2026 ese panorama ha cambiado por completo.

Hoy el enfoque dominante es lo que yo llamo gestión de estado híbrida. En vez de meter todo en una única solución monolítica, usas herramientas especializadas según el tipo de estado que necesites manejar. TanStack Query para datos del servidor, Zustand o Jotai para estado compartido del cliente, y useState/useReducer con Context para estado local y de entorno.

¿Los números? Zustand ha crecido más de un 30% interanual y aparece en alrededor del 40% de los proyectos nuevos. TanStack Query gestiona cerca del 80% de los datos en apps que lo adoptan. Y Redux escrito a mano ha bajado a apenas el 10% de los proyectos nuevos (aunque Redux Toolkit sigue firme en entornos empresariales, que conste).

En esta guía vamos a recorrer las herramientas de gestión de estado más relevantes para React Native en 2026. Veremos código práctico, patrones de persistencia con MMKV, integración con la Nueva Arquitectura, y las mejores prácticas para que tu app sea mantenible y performante. Así que, vamos al grano.

Los Tipos de Estado: La Clave para Elegir la Herramienta Correcta

Antes de elegir una librería, necesitas entender qué tipo de estado estás manejando. Y te lo digo porque este es, de lejos, el error más común que veo en proyectos React Native: tratar todo el estado de la misma manera.

Estado del Servidor (Server State)

Son los datos que vienen de una API, base de datos o servicio externo. Se caracterizan por ser asíncronos, potencialmente compartidos entre múltiples usuarios, y pueden quedar obsoletos sin que tu app lo sepa. Piensa en la lista de productos, el perfil de usuario desde el backend, o las notificaciones.

Herramienta recomendada: TanStack Query (React Query)

Estado del Cliente (Client State)

Son datos que existen únicamente en el cliente y que la app controla por completo. No se sincronizan con ningún servidor. El tema de la app (claro/oscuro), el estado de un sidebar, los filtros que seleccionó el usuario, el carrito de compras... ese tipo de cosas.

Herramientas recomendadas: Zustand, Jotai, o Context API

Estado Local de Componente

Estado que solo necesita un componente específico y sus hijos inmediatos. No tiene ningún sentido elevarlo a un store global. El valor de un input, si un modal está abierto o cerrado, una animación local.

Herramienta recomendada: useState y useReducer

Estado de Formulario

Estado especializado para manejar validación, errores, estados de envío y valores de campos. Formularios de registro, checkout, configuración del perfil... ya sabes.

Herramienta recomendada: React Hook Form o Formik

La regla de oro es simple: empieza con lo más simple posible y añade complejidad solo cuando la necesites. No instales Zustand para un contador. No uses Redux para guardar si un modal está abierto. Y sobre todo, no guardes datos de API en un store de cliente — para eso existe TanStack Query.

Zustand: El Equilibrio Perfecto entre Simplicidad y Potencia

¿Por Qué Zustand Domina en 2026?

Zustand (que significa "estado" en alemán, por si te lo preguntabas) se ha convertido en la librería de estado del cliente más popular en el ecosistema React Native. Y la verdad es que no es difícil entender por qué. Su API es minimalista, no requiere providers, el bundle es diminuto (alrededor de 1KB), y funciona de maravilla con TypeScript.

A diferencia de Redux, no hay actions, reducers, ni dispatchers. Creas un store, lo usas como un hook, y listo. Así de sencillo. Los componentes solo se re-renderizan cuando cambia el dato específico que consumen.

Tu Primer Store con Zustand

Vamos a crear un store de autenticación típico en React Native:

// stores/useAuthStore.ts
import { create } from 'zustand';

interface User {
  id: string;
  name: string;
  email: string;
  avatarUrl: string | null;
}

interface AuthState {
  user: User | null;
  token: string | null;
  isAuthenticated: boolean;
  login: (user: User, token: string) => void;
  logout: () => void;
  updateProfile: (updates: Partial<User>) => void;
}

export const useAuthStore = create<AuthState>((set) => ({
  user: null,
  token: null,
  isAuthenticated: false,

  login: (user, token) =>
    set({ user, token, isAuthenticated: true }),

  logout: () =>
    set({ user: null, token: null, isAuthenticated: false }),

  updateProfile: (updates) =>
    set((state) => ({
      user: state.user
        ? { ...state.user, ...updates }
        : null,
    })),
}));

Y usarlo en un componente es igual de sencillo:

// components/ProfileHeader.tsx
import { View, Text, Image } from 'react-native';
import { useAuthStore } from '../stores/useAuthStore';

export function ProfileHeader() {
  // Solo se re-renderiza cuando cambia user
  const user = useAuthStore((state) => state.user);
  const logout = useAuthStore((state) => state.logout);

  if (!user) return null;

  return (
    <View style={styles.header}>
      <Image
        source={{ uri: user.avatarUrl ?? 'https://placeholder.com/avatar' }}
        style={styles.avatar}
      />
      <Text style={styles.name}>{user.name}</Text>
      <Text style={styles.email}>{user.email}</Text>
      <Pressable onPress={logout}>
        <Text>Cerrar sesión</Text>
      </Pressable>
    </View>
  );
}

Fíjate en un detalle clave: estamos usando selectores individuales (state => state.user) en lugar de desestructurar todo el store. Esto es súper importante porque garantiza que el componente solo se re-renderice cuando el valor seleccionado cambie, no cuando cualquier otra parte del store se modifique.

Persistencia con MMKV: 30x Más Rápido que AsyncStorage

Una de las ventajas más poderosas de Zustand en React Native es su middleware de persistencia. Y cuando lo combinas con MMKV en lugar de AsyncStorage, obtienes una persistencia que es entre 30 y 100 veces más rápida. No es exageración.

Primero, instala la dependencia:

npx expo install react-native-mmkv

Ahora crea un adaptador de almacenamiento y configura tu store persistente:

// stores/storage.ts
import { MMKV } from 'react-native-mmkv';
import { StateStorage } from 'zustand/middleware';

const mmkv = new MMKV();

export const mmkvStorage: StateStorage = {
  getItem: (name) => {
    const value = mmkv.getString(name);
    return value ?? null;
  },
  setItem: (name, value) => {
    mmkv.set(name, value);
  },
  removeItem: (name) => {
    mmkv.delete(name);
  },
};
// stores/useSettingsStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { mmkvStorage } from './storage';

interface SettingsState {
  theme: 'light' | 'dark' | 'system';
  language: string;
  notificationsEnabled: boolean;
  setTheme: (theme: 'light' | 'dark' | 'system') => void;
  setLanguage: (language: string) => void;
  toggleNotifications: () => void;
}

export const useSettingsStore = create<SettingsState>()(
  persist(
    (set) => ({
      theme: 'system',
      language: 'es',
      notificationsEnabled: true,

      setTheme: (theme) => set({ theme }),
      setLanguage: (language) => set({ language }),
      toggleNotifications: () =>
        set((state) => ({
          notificationsEnabled: !state.notificationsEnabled,
        })),
    }),
    {
      name: 'app-settings',
      storage: createJSONStorage(() => mmkvStorage),
    }
  )
);

Con esta configuración, las preferencias del usuario sobreviven al cierre de la app y se cargan casi instantáneamente al reiniciar. La clave está en que MMKV es síncrono — no hay promesas ni estados de carga intermedios. Simplemente funciona.

Patrón Avanzado: Slices para Stores Grandes

Cuando tu app crece (y créeme, siempre crecen más de lo que planeas), un único store monolítico se vuelve difícil de mantener. Zustand soporta un patrón de slices que te permite dividir el store en partes manejables:

// stores/slices/cartSlice.ts
import { StateCreator } from 'zustand';

export interface CartItem {
  productId: string;
  name: string;
  price: number;
  quantity: number;
}

export interface CartSlice {
  items: CartItem[];
  addItem: (item: Omit<CartItem, 'quantity'>) => void;
  removeItem: (productId: string) => void;
  updateQuantity: (productId: string, quantity: number) => void;
  clearCart: () => void;
  totalPrice: () => number;
}

export const createCartSlice: StateCreator<CartSlice> = (set, get) => ({
  items: [],

  addItem: (item) =>
    set((state) => {
      const existing = state.items.find(
        (i) => i.productId === item.productId
      );
      if (existing) {
        return {
          items: state.items.map((i) =>
            i.productId === item.productId
              ? { ...i, quantity: i.quantity + 1 }
              : i
          ),
        };
      }
      return { items: [...state.items, { ...item, quantity: 1 }] };
    }),

  removeItem: (productId) =>
    set((state) => ({
      items: state.items.filter((i) => i.productId !== productId),
    })),

  updateQuantity: (productId, quantity) =>
    set((state) => ({
      items: state.items.map((i) =>
        i.productId === productId ? { ...i, quantity } : i
      ),
    })),

  clearCart: () => set({ items: [] }),

  totalPrice: () =>
    get().items.reduce((sum, item) => sum + item.price * item.quantity, 0),
});

Luego combinas los slices en un único store:

// stores/useAppStore.ts
import { create } from 'zustand';
import { createCartSlice, CartSlice } from './slices/cartSlice';
// Importar otros slices aquí

type AppStore = CartSlice; // & OtherSlice & AnotherSlice

export const useAppStore = create<AppStore>()((...args) => ({
  ...createCartSlice(...args),
  // ...createOtherSlice(...args),
}));

Jotai: Reactividad Atómica y Granular

Cuando Necesitas Control a Nivel de Átomo

Jotai (que significa "estado" en japonés — sí, a los creadores de estas librerías les gusta ponerle "estado" en otros idiomas) toma un enfoque completamente diferente al de Zustand. En lugar de crear un store centralizado, defines "átomos": pequeñas unidades independientes de estado. Los componentes solo se suscriben a los átomos específicos que usan, lo que resulta en re-renderizados extremadamente granulares.

Con un bundle de apenas 4KB, Jotai es ideal para apps donde el rendimiento es crítico y donde necesitas un control fino sobre qué partes de la UI se actualizan.

Átomos Básicos y Derivados

// atoms/todoAtoms.ts
import { atom } from 'jotai';

// Átomo primitivo
export const todosAtom = atom<Todo[]>([]);

// Átomo derivado (solo lectura)
export const completedTodosAtom = atom((get) => {
  const todos = get(todosAtom);
  return todos.filter((todo) => todo.completed);
});

// Átomo derivado con conteo
export const todoStatsAtom = atom((get) => {
  const todos = get(todosAtom);
  return {
    total: todos.length,
    completed: todos.filter((t) => t.completed).length,
    pending: todos.filter((t) => !t.completed).length,
  };
});

// Átomo con escritura personalizada
export const addTodoAtom = atom(
  null, // sin lectura
  (get, set, title: string) => {
    const todos = get(todosAtom);
    set(todosAtom, [
      ...todos,
      {
        id: Date.now().toString(),
        title,
        completed: false,
      },
    ]);
  }
);

El uso en componentes es limpio y directo:

// components/TodoList.tsx
import { useAtom, useAtomValue } from 'jotai';
import { todosAtom, todoStatsAtom, addTodoAtom } from '../atoms/todoAtoms';

function TodoStats() {
  // Solo se re-renderiza cuando cambian las estadísticas
  const stats = useAtomValue(todoStatsAtom);

  return (
    <View style={styles.stats}>
      <Text>Total: {stats.total}</Text>
      <Text>Completados: {stats.completed}</Text>
      <Text>Pendientes: {stats.pending}</Text>
    </View>
  );
}

function AddTodoButton() {
  const [, addTodo] = useAtom(addTodoAtom);

  return (
    <Pressable onPress={() => addTodo('Nueva tarea')}>
      <Text>Agregar tarea</Text>
    </Pressable>
  );
}

Átomos Asíncronos con Suspense

Una de las características más interesantes de Jotai es su soporte nativo para operaciones asíncronas usando React Suspense. Esto simplifica mucho el manejo de datos remotos:

// atoms/userAtoms.ts
import { atom } from 'jotai';

const userIdAtom = atom<string | null>(null);

// Átomo asíncrono que se resuelve automáticamente
const userProfileAtom = atom(async (get) => {
  const userId = get(userIdAtom);
  if (!userId) return null;

  const response = await fetch(
    `https://api.miapp.com/users/${userId}`
  );
  return response.json();
});

// En el componente
import { Suspense } from 'react';
import { useAtomValue } from 'jotai';

function UserProfile() {
  const profile = useAtomValue(userProfileAtom);

  if (!profile) return <Text>Selecciona un usuario</Text>;

  return (
    <View>
      <Text>{profile.name}</Text>
      <Text>{profile.email}</Text>
    </View>
  );
}

// Envolver con Suspense
function UserScreen() {
  return (
    <Suspense fallback={<ActivityIndicator />}>
      <UserProfile />
    </Suspense>
  );
}

Persistencia con atomWithStorage

Jotai incluye una utilidad atomWithStorage que funciona con AsyncStorage en React Native. La configuración es bastante directa:

// atoms/persistedAtoms.ts
import { atomWithStorage, createJSONStorage } from 'jotai/utils';
import AsyncStorage from '@react-native-async-storage/async-storage';

const storage = createJSONStorage<string>(() => AsyncStorage);

export const themeAtom = atomWithStorage<'light' | 'dark'>(
  'app-theme',
  'light',
  storage
);

export const onboardingCompletedAtom = atomWithStorage<boolean>(
  'onboarding-completed',
  false,
  storage
);

¿Zustand o Jotai? Cuándo Usar Cada Uno

Ambas librerías son del mismo equipo (pmndrs) y se complementan bien. La regla general que yo sigo es esta:

  • Zustand: Cuando tienes estado relacionado que se actualiza junto. Piensa en autenticación, carrito de compras, configuración. Es como tener un objeto con métodos que lo modifican.
  • Jotai: Cuando tienes muchos pedazos de estado independientes que diferentes partes de la UI necesitan de forma granular. Es ideal para dashboards, editores, o interfaces con muchos widgets independientes.

En la práctica, muchas apps usan ambas sin problema: Zustand para stores de dominio y Jotai para estado de UI granular. No hay conflicto.

TanStack Query: El Rey del Estado del Servidor

¿Por Qué No Debes Guardar Datos de API en Zustand o Redux?

Este es, posiblemente, el error más frecuente en apps React Native: hacer un fetch, guardar el resultado en un store de Redux o Zustand, y gestionar manualmente el loading, error, caché, re-fetching, invalidación, paginación y actualizaciones optimistas. Es una cantidad absurda de código boilerplate para algo que TanStack Query resuelve en pocas líneas.

TanStack Query (anteriormente conocido como React Query) separa el concepto de "estado del servidor" del "estado del cliente", y te da cacheo automático, re-fetching inteligente, actualizaciones en segundo plano, paginación infinita y mucho más. Todo declarativo, todo basado en hooks.

Configuración en React Native

La configuración de TanStack Query en React Native requiere un paso extra comparado con la web: necesitas configurar manualmente el onlineManager y el focusManager porque React Native no tiene las mismas APIs de ventana que un navegador. Un detalle menor, pero importante.

// providers/QueryProvider.tsx
import { QueryClient, QueryClientProvider, focusManager, onlineManager } from '@tanstack/react-query';
import { useEffect } from 'react';
import { AppState, Platform } from 'react-native';
import type { AppStateStatus } from 'react-native';
import NetInfo from '@react-native-community/netinfo';

// Configurar el online manager
onlineManager.setEventListener((setOnline) => {
  return NetInfo.addEventListener((state) => {
    setOnline(!!state.isConnected);
  });
});

// Hook para gestionar el foco de la app
function useAppStateFocus() {
  useEffect(() => {
    const subscription = AppState.addEventListener(
      'change',
      (status: AppStateStatus) => {
        if (Platform.OS !== 'web') {
          focusManager.setFocused(status === 'active');
        }
      }
    );
    return () => subscription.remove();
  }, []);
}

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5 minutos
      gcTime: 1000 * 60 * 30,   // 30 minutos
      retry: 2,
      refetchOnWindowFocus: true,
      refetchOnReconnect: true,
    },
  },
});

export function QueryProvider({ children }: { children: React.ReactNode }) {
  useAppStateFocus();

  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );
}

Queries: Obtener Datos de Forma Declarativa

// hooks/useProducts.ts
import { useQuery, useInfiniteQuery } from '@tanstack/react-query';
import { api } from '../services/api';

// Query simple
export function useProduct(productId: string) {
  return useQuery({
    queryKey: ['product', productId],
    queryFn: () => api.getProduct(productId),
    enabled: !!productId, // No ejecutar si no hay ID
  });
}

// Query con lista paginada infinita
export function useProducts(categoryId?: string) {
  return useInfiniteQuery({
    queryKey: ['products', { categoryId }],
    queryFn: ({ pageParam }) =>
      api.getProducts({ page: pageParam, categoryId }),
    initialPageParam: 1,
    getNextPageParam: (lastPage) =>
      lastPage.hasMore ? lastPage.nextPage : undefined,
  });
}

Usar estas queries en un componente es directo y, la verdad, bastante elegante:

// screens/ProductListScreen.tsx
import { FlatList, ActivityIndicator, Text } from 'react-native';
import { useProducts } from '../hooks/useProducts';

export function ProductListScreen() {
  const {
    data,
    isLoading,
    isError,
    error,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useProducts();

  if (isLoading) return <ActivityIndicator size="large" />;
  if (isError) return <Text>Error: {error.message}</Text>;

  const products = data?.pages.flatMap((page) => page.items) ?? [];

  return (
    <FlatList
      data={products}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => <ProductCard product={item} />}
      onEndReached={() => {
        if (hasNextPage) fetchNextPage();
      }}
      onEndReachedThreshold={0.5}
      ListFooterComponent={
        isFetchingNextPage ? <ActivityIndicator /> : null
      }
    />
  );
}

Mutations: Modificar Datos con Actualizaciones Optimistas

Las mutations de TanStack Query manejan operaciones de escritura (POST, PUT, DELETE) con soporte integrado para actualizaciones optimistas. Es decir, puedes mostrar el resultado esperado al usuario antes de que el servidor confirme la operación:

// hooks/useToggleFavorite.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '../services/api';

export function useToggleFavorite() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (productId: string) =>
      api.toggleFavorite(productId),

    // Actualización optimista
    onMutate: async (productId) => {
      // Cancelar queries en curso para evitar sobrescribir
      await queryClient.cancelQueries({
        queryKey: ['product', productId],
      });

      // Guardar el estado anterior
      const previousProduct = queryClient.getQueryData(
        ['product', productId]
      );

      // Actualizar optimistamente
      queryClient.setQueryData(
        ['product', productId],
        (old: any) => ({
          ...old,
          isFavorite: !old.isFavorite,
        })
      );

      return { previousProduct };
    },

    // Revertir si hay error
    onError: (_err, productId, context) => {
      queryClient.setQueryData(
        ['product', productId],
        context?.previousProduct
      );
    },

    // Invalidar para refrescar desde el servidor
    onSettled: (_data, _error, productId) => {
      queryClient.invalidateQueries({
        queryKey: ['product', productId],
      });
    },
  });
}

Combinando Todo: La Arquitectura de Estado Completa

Estructura de Carpetas Recomendada

Una estructura clara facilita la mantenibilidad a medida que la app crece. Esta es la que me ha funcionado mejor en proyectos reales:

src/
├── stores/              # Estado del cliente (Zustand)
│   ├── useAuthStore.ts
│   ├── useSettingsStore.ts
│   ├── storage.ts       # Adaptador MMKV
│   └── slices/
│       └── cartSlice.ts
├── atoms/               # Estado atómico (Jotai, si lo usas)
│   ├── uiAtoms.ts
│   └── filterAtoms.ts
├── hooks/               # Queries y mutations (TanStack Query)
│   ├── useProducts.ts
│   ├── useUser.ts
│   └── useOrders.ts
├── services/            # Funciones de API
│   └── api.ts
├── providers/           # Providers de la app
│   └── QueryProvider.tsx
└── app/
    └── _layout.tsx      # Layout raíz con providers

El Layout Raíz con Todos los Providers

// app/_layout.tsx
import { Stack } from 'expo-router';
import { QueryProvider } from '../providers/QueryProvider';

export default function RootLayout() {
  return (
    <QueryProvider>
      <Stack>
        <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
        <Stack.Screen name="modal" options={{ presentation: 'modal' }} />
      </Stack>
    </QueryProvider>
  );
}

Un detalle que me encanta de esta arquitectura: Zustand y Jotai no necesitan providers. Los stores de Zustand son hooks globales y los átomos de Jotai funcionan sin un Provider explícito (aunque puedes usar uno si quieres aislar estados en tests o en sub-árboles de componentes). Menos boilerplate, menos anidamiento de providers.

Patrón: Combinar Estado del Servidor con Estado del Cliente

Un patrón muy común es combinar datos de TanStack Query con estado local de Zustand. Por ejemplo, un carrito de compras donde los productos vienen del servidor pero la selección del usuario es estado del cliente:

// screens/CartScreen.tsx
import { useQuery } from '@tanstack/react-query';
import { useAppStore } from '../stores/useAppStore';
import { api } from '../services/api';

export function CartScreen() {
  // Estado del cliente: items en el carrito
  const cartItems = useAppStore((s) => s.items);
  const removeItem = useAppStore((s) => s.removeItem);
  const totalPrice = useAppStore((s) => s.totalPrice);

  // Estado del servidor: detalles actualizados de los productos
  const productIds = cartItems.map((item) => item.productId);
  const { data: freshProducts } = useQuery({
    queryKey: ['products', 'batch', productIds],
    queryFn: () => api.getProductsByIds(productIds),
    enabled: productIds.length > 0,
  });

  // Combinar: cantidades del carrito + precios actualizados del servidor
  const enrichedItems = cartItems.map((cartItem) => {
    const fresh = freshProducts?.find(
      (p) => p.id === cartItem.productId
    );
    return {
      ...cartItem,
      currentPrice: fresh?.price ?? cartItem.price,
      inStock: fresh?.inStock ?? true,
    };
  });

  return (
    <FlatList
      data={enrichedItems}
      renderItem={({ item }) => (
        <CartItemRow
          item={item}
          onRemove={() => removeItem(item.productId)}
        />
      )}
      ListFooterComponent={
        <Text style={styles.total}>
          Total: ${totalPrice().toFixed(2)}
        </Text>
      }
    />
  );
}

Legend-State: La Alternativa de Alto Rendimiento

Vale la pena mencionar Legend-State como una alternativa emergente que está ganando tracción. Con solo 4KB, esta librería basada en observables ofrece reactividad de grano fino sin providers, contexts, actions, reducers ni dispatchers.

// Con Legend-State, la sintaxis es directa
import { observable } from '@legendapp/state';
import { observer } from '@legendapp/state/react';

const state$ = observable({
  count: 0,
  user: { name: 'Ana' },
});

// Los componentes se envuelven con observer
const Counter = observer(function Counter() {
  // get() obtiene el valor y suscribe al componente
  const count = state$.count.get();

  return (
    <Pressable onPress={() => state$.count.set((c) => c + 1)}>
      <Text>Contador: {count}</Text>
    </Pressable>
  );
});

Legend-State incluye plugins de persistencia para MMKV y sincronización con backends como Supabase, lo que lo hace especialmente atractivo para apps con enfoque local-first. Si estás construyendo algo donde el rendimiento de las actualizaciones de estado es absolutamente crítico, merece la pena que le eches un vistazo.

Errores Comunes y Cómo Evitarlos

1. Guardar Datos del Servidor en el Store del Cliente

// ❌ MAL: Guardar respuesta de API en Zustand
const useProductStore = create((set) => ({
  products: [],
  loading: false,
  fetchProducts: async () => {
    set({ loading: true });
    const products = await api.getProducts();
    set({ products, loading: false });
  },
}));

// ✅ BIEN: Usar TanStack Query para datos del servidor
function useProducts() {
  return useQuery({
    queryKey: ['products'],
    queryFn: api.getProducts,
  });
}

2. No Usar Selectores en Zustand

// ❌ MAL: Desestructurar todo el store
const { user, theme, cart } = useAppStore();

// ✅ BIEN: Seleccionar solo lo necesario
const user = useAppStore((s) => s.user);
const theme = useAppStore((s) => s.theme);

3. Estado Global para Todo

// ❌ MAL: Estado de modal en store global
const useUIStore = create((set) => ({
  isModalOpen: false,
  toggleModal: () =>
    set((s) => ({ isModalOpen: !s.isModalOpen })),
}));

// ✅ BIEN: Estado local del componente
function MyScreen() {
  const [isModalOpen, setModalOpen] = useState(false);
  // ...
}

4. No Configurar staleTime en TanStack Query

// ❌ MAL: staleTime por defecto es 0 (siempre stale)
useQuery({ queryKey: ['user'], queryFn: fetchUser });

// ✅ BIEN: Configurar staleTime según el caso
useQuery({
  queryKey: ['user'],
  queryFn: fetchUser,
  staleTime: 1000 * 60 * 5, // El perfil no cambia cada segundo
});

Tabla Comparativa: ¿Qué Herramienta Elegir?

Para facilitar tu decisión, aquí tienes una comparación directa de las principales opciones:

  • Zustand (~1KB): Ideal para estado del cliente compartido. API simple basada en hooks, sin providers. Excelente con TypeScript. Perfecto para la mayoría de apps.
  • Jotai (~4KB): Ideal para estado granular y atómico. Renders ultra-específicos. Soporte nativo de async con Suspense. Perfecto para UIs complejas con muchos estados independientes.
  • TanStack Query (~13KB): El estándar para estado del servidor. Cache, refetching, paginación infinita, mutations optimistas. Imprescindible en cualquier app con API.
  • Redux Toolkit (~11KB): Sigue siendo válido en apps enterprise grandes. Devtools maduros, middleware extensible. Usa RTK Query para datos del servidor.
  • Legend-State (~4KB): Basado en observables. Reactividad de grano fino. Persistencia integrada. Ideal para apps local-first.
  • Context API (0KB): Incluido en React. Perfecto para estado que cambia poco (tema, idioma, auth). No usar para estado que cambia frecuentemente — provoca re-renders innecesarios.

Conclusión: La Estrategia Ganadora en 2026

La gestión de estado en React Native ya no es una decisión binaria entre "usar Redux o no". En 2026, la estrategia más efectiva es una combinación inteligente de herramientas especializadas:

  1. TanStack Query para todo lo que viene de una API. Sinceramente, no hay excusa para no usarlo — elimina una cantidad enorme de código boilerplate y te da cacheo, re-fetching y actualizaciones optimistas gratis.
  2. Zustand (o Jotai) para el estado del cliente que necesita ser compartido entre componentes. Elige Zustand si prefieres stores centralizados, o Jotai si necesitas reactividad atómica.
  3. useState/useReducer para estado local que no sale del componente. No compliques lo que no necesita ser complicado.
  4. MMKV para persistencia que necesita ser rápida y síncrona.

Empieza con lo más simple posible. Añade complejidad solo cuando tengas un problema real que resolver. Y sobre todo, elige la herramienta según el tipo de estado que estés manejando, no según lo que esté de moda en Twitter.

Si ya leíste nuestros artículos sobre navegación con Expo Router y React Navigation 7 y sobre rendimiento con la Nueva Arquitectura y React Compiler, con esta guía de gestión de estado tienes los tres pilares fundamentales para construir apps React Native robustas y modernas en 2026. La base está lista — ahora te toca a ti construir.

Sobre el Autor Editorial Team

Our team of expert writers and editors.