Redux Toolkit
Estado predecible para equipos grandes y depuración de viaje temporal. Redux Toolkit (RTK) se amortiza cuando cumplimiento , ecosistemas de middleware , o inversión Redux existente demandan reductores centralizados. Para aplicaciones nuevas de Expo SDK 57, opta por TanStack Query + Zustand a menos que un ADR obligue RTK.
Tarjeta de referencia rápida - lista para copiar-pegar.
npx expo install @reduxjs/toolkit react-redux
// src/app/store.ts
import { configureStore } from "@reduxjs/toolkit" ;
import { cartSlice } from "@/features/cart/cartSlice" ;
import { checkoutSlice } from "@/features/checkout/checkoutSlice" ;
export const store = configureStore ({
reducer: {
cart: cartSlice.reducer,
checkout: checkoutSlice.reducer,
},
devTools: __DEV__,
});
export type RootState = ReturnType < typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// app/_layout.tsx
import { Provider } from "react-redux" ;
import { Stack } from "expo-router" ;
import { store } from "@/app/store" ;
export default function RootLayout () {
return (
< Provider store = {store}>
< Stack />
</ Provider >
);
}
// features/cart/cartSlice.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit" ;
type CartItem = { productId : string ; qty : number };
type CartState = { items : CartItem [] };
const initialState : CartState = { items: [] };
export const cartSlice = createSlice ({
name: "cart" ,
initialState,
reducers: {
addItem ( state , action : PayloadAction < CartItem >) {
state.items. push (action.payload);
},
clearCart ( state ) {
state.items = [];
},
},
});
export const { addItem , clearCart } = cartSlice.actions;
// Hooks tipados
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux" ;
import type { AppDispatch, RootState } from "@/app/store" ;
export const useAppDispatch = () => useDispatch < AppDispatch >();
export const useAppSelector : TypedUseSelectorHook < RootState > = useSelector;
Cuándo usarlo:
Dominios regulados necesitan registros de acciones y reproducción (fintech, flujos de workflows de salud).
Múltiples equipos ya estandarizados en patrones y materiales de capacitación de Redux.
Requisitos de middleware : análisis por acción, migas de pan de fallo, integración de cola offline.
RTK Query es aceptable cuando la aplicación completa está en Redux - de lo contrario usa TanStack Query.
ADR clasifica explícitamente RTK sobre Zustand - ver ADR .
// checkoutSlice.ts - flujo de cliente de múltiples pasos con acciones explícitas
import { createSlice, PayloadAction } from "@reduxjs/toolkit" ;
type Step = "cart" | "shipping" | "payment" | "done" ;
type CheckoutState = {
step : Step ;
shippingId : string | null ;
error : string | null ;
};
const initialState : CheckoutState = {
step: "cart" ,
shippingId: null ,
error: null ,
};
export const checkoutSlice = createSlice ({
name: "checkout" ,
initialState,
reducers: {
setStep ( state , action : PayloadAction < Step >) {
state.step = action.payload;
state.error = null ;
},
selectShipping ( state , action : PayloadAction < string >) {
state.shippingId = action.payload;
},
setError ( state , action : PayloadAction < string >) {
state.error = action.payload;
},
resetCheckout () {
return initialState;
},
},
});
export const { setStep , selectShipping , setError , resetCheckout } = checkoutSlice.actions;
// CheckoutScreen.tsx - métodos de envío del servidor vía TanStack Query; pasos vía Redux
import { useQuery } from "@tanstack/react-query" ;
import { Pressable, Text, View } from "react-native" ;
import { useAppDispatch, useAppSelector } from "@/app/hooks" ;
import { addItem, clearCart } from "@/features/cart/cartSlice" ;
import { resetCheckout, selectShipping, setStep } from "@/features/checkout/checkoutSlice" ;
function fetchShippingMethods () {
return Promise . resolve ([
{ id: "standard" , label: "Estándar" },
{ id: "express" , label: "Expresión" },
]);
}
export function CheckoutScreen () {
const dispatch = useAppDispatch ();
const step = useAppSelector (( s ) => s.checkout.step);
const shippingId = useAppSelector (( s ) => s.checkout.shippingId);
const cartCount = useAppSelector (( s ) => s.cart.items. length );
const { data : methods = [] } = useQuery ({
queryKey: [ "shipping-methods" ],
queryFn: fetchShippingMethods,
enabled: step === "shipping" ,
});
return (
< View style = {{ padding: 16 , gap: 8 }}>
< Text >Paso: {step}</ Text >
< Text >Artículos del carrito: {cartCount}</ Text >
{step === "cart" && (
< Pressable
onPress = {() => {
dispatch ( addItem ({ productId: "demo" , qty: 1 }));
dispatch ( setStep ( "shipping" ));
}}
>
< Text >Continuar al envío</ Text >
</ Pressable >
)}
{step === "shipping" &&
methods. map (( m ) => (
< Pressable
key = {m.id}
onPress = {() => {
dispatch ( selectShipping (m.id));
dispatch ( setStep ( "payment" ));
}}
>
< Text style = {{ fontWeight: shippingId === m.id ? "700" : "400" }}>{m.label}</ Text >
</ Pressable >
))}
{step === "payment" && (
< Pressable
onPress = {() => {
dispatch ( clearCart ());
dispatch ( resetCheckout ());
}}
>
< Text >Pagar (demostración)</ Text >
</ Pressable >
)}
</ View >
);
}
// checkoutSlice.test.ts
import { checkoutSlice, setStep } from "./checkoutSlice" ;
test ( "setStep limpia el error" , () => {
const state = checkoutSlice. reducer (
{ step: "cart" , shippingId: null , error: "fail" },
setStep ( "shipping" )
);
expect (state.step). toBe ( "shipping" );
expect (state.error). toBeNull ();
});
Lo que esto demuestra:
Los slices RTK codifican transiciones de checkout - nombres de acciones auditables en DevTools.
TanStack Query sigue siendo el propietario de los métodos de envío desde API - no duplicado en Redux.
Los hooks tipados previenen el envío de formas de acciones incorrectas.
Las pruebas de reductores se ejecutan sin React - señal rápida de CI.
resetCheckout devuelve el estado inicial en la finalización - límite de dominio claro.
configureStore añade:
Middleware Redux Thunk para lógica asincrónica
Integración de DevTools en desarrollo
Comprobaciones de inmutabilidad y serializabilidad (aviso en dev)
Desactiva comprobaciones ruidosas para acciones de persistencia si es necesario:
middleware : ( getDefaultMiddleware ) =>
getDefaultMiddleware ({
serializableCheck: { ignoredActions: [ "persist/PERSIST" ] },
}),
Si la organización requiere Redux para todos los datos:
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react" ;
export const api = createApi ({
reducerPath: "api" ,
baseQuery: fetchBaseQuery ({ baseUrl: "https://api.example.com" }),
endpoints : ( builder ) => ({
getOrders: builder. query < Order [], void >({ query : () => "/orders" }),
}),
});
// store reducer: { [api.reducerPath]: api.reducer }
// middleware: getDefaultMiddleware().concat(api.middleware)
Prefiere TanStack Query en aplicaciones nuevas de Expo a menos que RTK esté ya en todas partes - dos capas de caché compiten entre sí.
Ejecuta la aplicación en desarrollo con Redux DevTools (complemento de Flipper o depurador remoto).
Envía acciones - árbol de estado se actualiza por slice.
Salta a la acción anterior - reproduce informes de errores con secuencia exacta.
Crítico para escenarios de auditoría donde el soporte debe reproducir pasos de usuario.
import { createEntityAdapter, createSlice } from "@reduxjs/toolkit" ;
const ordersAdapter = createEntityAdapter < Order >();
const ordersSlice = createSlice ({
name: "orders" ,
initialState: ordersAdapter. getInitialState (),
reducers: {
ordersLoaded: ordersAdapter.setAll,
orderUpdated: ordersAdapter.updateOne,
},
});
Sigue prefiriendo Query para listas de servidor a menos que middleware offline requiera entidades Redux normalizadas.
Reacciona a acciones sin acoplar slices:
import { createListenerMiddleware } from "@reduxjs/toolkit" ;
const listenerMiddleware = createListenerMiddleware ();
listenerMiddleware. startListening ({
actionCreator: clearCart,
effect : async ( _action , listenerApi ) => {
listenerApi. dispatch ( resetCheckout ());
},
});
Obtener datos en componentes en Redux en cada montaje - Duplica Query. Fix: Query para servidor; Redux solo para flujo de cliente.
Reductores raíz gigantes - Un archivo slice por característica. Fix: combineReducers vía mapa de reductores de configureStore.
Valores no serializables en estado - Date, instancias de clase rompen DevTools. Fix: Almacena cadenas ISO; asigna en selectores.
useSelector sin función de igualdad - Devolver nuevos objetos re-renderiza a menudo. Fix: Selecciona primitivos o usa shallowEqual.
redux-persist con tokens en AsyncStorage - Fallo de revisión de seguridad. Fix: Almacén seguro para secretos; ver guía de persistencia.
Instalando RTK "solo en caso" - Impuesto de bundle y repetición. Fix: ADR antes de la adopción.
Dos cachés de servidor - RTK Query + TanStack Query para los mismos endpoints. Fix: Elige uno.
Alternativa Usarla cuando No la uses cuando Redux Toolkit Auditoría, middleware, estándares de equipo Solo carrito + tema Zustand Estado de cliente global ligero Registros de acción obligatorios TanStack Query Estado del servidor (predeterminado) Máquina de pasos de checkout sola useReducer + Context Pequeña interfaz de usuario compartida Ledger auditado entre características Event sourcing Registro inmutable de cumplimiento Aplicación de consumidor típica
RTK vs tamaño de bundle de Zustand?
RTK + react-redux es más grande que Zustand. Justificado cuando DevTools y middleware son requeridos - no para un contador de insignia.
¿Deberían las aplicaciones nuevas de Expo comenzar con Redux?
No - Query + Zustand predeterminado por ADR Decision 10 . Adopta RTK cuando aparezcan requisitos.
RTK Query vs TanStack Query?
La paridad de características es cercana. TanStack Query tiene documentación más fuerte de Expo/mobile en este sitio. RTK Query cuando los reductores ya son dueños de la aplicación.
¿Cómo escribo useSelector?
Exporta RootState desde almacenamiento; usa TypedUseSelectorHook<RootState> - ver hooks tipados de receta.
¿Aún thunks asincrónico?
createAsyncThunk funciona. Muchos equipos usan mutaciones de Query en lugar de thunks para red - thunks para orquestación multi-slice compleja.
Redux DevTools en dispositivo físico?
Usa complemento de Redux de Flipper o depuración remota de JS. Los builds de producción desactivan DevTools vía devTools: __DEV__.
¿Cómo pruebo pantallas conectadas?
import { Provider } from "react-redux" ;
import { configureStore } from "@reduxjs/toolkit" ;
const store = configureStore ({ reducer: { cart: cartSlice.reducer } });
render (< Provider store = {store}>< CheckoutScreen /></ Provider >);
¿Pueden los slices vivir en carpetas de características?
Sí - features/cart/cartSlice.ts importado en app/store.ts. Mantiene características modulares.
¿Immer ya dentro de createSlice?
Sí - mueve state en reductores; Immer produce actualizaciones inmutables.
¿Secuencia de logout?
Envía resetCheckout, clearCart, y api.util.resetApiState() si usas RTK Query; más queryClient.clear() si TanStack Query coexiste durante migración.
¿Migrando de Redux a Zustand?
Slice por slice - mueve interfaz de usuario de lectura pesada a Query primero, luego slices de cliente a Zustand. Ejecuta lista de verificación de refactorización .
¿Expo Router con Redux?
Provider única en app/_layout.tsx. Las rutas envían acciones; evita importaciones de almacenamiento en tipos de entidad compartida.
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).