Gestión de Estado
Redux Store Configuration
Configuración Principal
// src/store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import authSlice from './slices/authSlice';
import menuSlice from './slices/menuSlice';
import tempDataSlice from './slices/tempDataSlice';
const persistConfig = {
key: 'root',
storage,
whitelist: ['auth', 'menu', 'tempData']
};
const rootReducer = combineReducers({
auth: authSlice,
menu: menuSlice,
tempData: tempDataSlice
});
const persistedReducer = persistReducer(persistConfig, rootReducer);
export const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
}
})
});
Auth Slice
Estado de Autenticación
// src/store/slices/authSlice.ts
interface AuthState {
user: User | null;
profiles: Profile[];
token: string | null;
isAuthenticated: boolean;
}
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
setUser: (state, action) => {
state.user = action.payload.user;
state.token = action.payload.token;
state.isAuthenticated = true;
},
setProfiles: (state, action) => {
state.profiles = action.payload;
},
logout: (state) => {
state.user = null;
state.token = null;
state.isAuthenticated = false;
state.profiles = [];
}
}
});
Selectores
export const getUser = (state: RootState) => state.auth;
export const getToken = (state: RootState) => state.auth.token;
export const getProfiles = (state: RootState) => state.auth.profiles;
export const isAuthenticated = (state: RootState) => state.auth.isAuthenticated;
Menu Slice
Estado de Navegación
// src/store/slices/menuSlice.ts
interface MenuState {
menuItems: MenuItem[];
activeRoute: string;
sidebarCollapsed: boolean;
}
const menuSlice = createSlice({
name: 'menu',
initialState,
reducers: {
setMenuItems: (state, action) => {
state.menuItems = action.payload;
},
setActiveRoute: (state, action) => {
state.activeRoute = action.payload;
},
toggleSidebar: (state) => {
state.sidebarCollapsed = !state.sidebarCollapsed;
}
}
});
Custom Hooks
useRedux Hook
// src/hooks/useRedux.tsx
export const useRedux = () => {
const dispatch = useAppDispatch();
const selector = useAppSelector;
return { dispatch, selector };
};
useAuth Hook
// src/hooks/useAuth.tsx
export const useAuth = () => {
const { user, isAuthenticated } = useSelector(getUser);
const dispatch = useDispatch();
const login = useCallback((credentials) => {
// Lógica de login
}, [dispatch]);
const logout = useCallback(() => {
dispatch(authSlice.actions.logout());
}, [dispatch]);
return { user, isAuthenticated, login, logout };
};
useMenu Hook
// src/hooks/useMenu.tsx
export const useMenu = () => {
const menuItems = useSelector(getMenuItems);
const dispatch = useDispatch();
const setActiveRoute = useCallback((route: string) => {
dispatch(menuSlice.actions.setActiveRoute(route));
}, [dispatch]);
return { menuItems, setActiveRoute };
};
Persistencia de Datos
Configuración Redux Persist
const persistConfig = {
key: 'root',
storage,
whitelist: ['auth', 'menu', 'tempData'], // Solo persistir estos slices
blacklist: ['notifications'] // No persistir notificaciones
};
Hidratación del Estado
// app/providers.tsx
export function Providers({ children }: { children: React.ReactNode }) {
return (
<Provider store={store}>
<PersistGate loading={<Spinner />} persistor={persistor}>
<ChakraProvider theme={theme}>
{children}
</ChakraProvider>
</PersistGate>
</Provider>
);
}
Estado Local vs Global
Cuándo usar Redux
- Estado global: Autenticación, menús, configuración
- Estado compartido: Datos entre múltiples componentes
- Estado persistente: Datos que deben sobrevivir al cierre
Cuándo usar useState
- Estado local: Formularios, modales, toggles
- Estado temporal: Datos que no se comparten
- Estado de UI: Loading states, errores locales
Ejemplo de Decisión
// ✅ Estado global - Información del usuario
const user = useSelector(getUser);
// ✅ Estado local - Estado de formulario
const [formData, setFormData] = useState({});
// ✅ Estado local - Loading específico
const [isLoading, setIsLoading] = useState(false);
Middleware y Efectos
Configuración de Middleware
export const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
}
}).concat(
// Middleware personalizados
authMiddleware,
loggerMiddleware
)
});
Auth Middleware
const authMiddleware: Middleware = (store) => (next) => (action) => {
if (action.type === 'auth/logout') {
// Limpiar localStorage y redirigir
localStorage.clear();
window.location.href = '/auth/sign-in';
}
return next(action);
};
Optimización del Estado
Normalización de Datos
// En lugar de arrays anidados
const badState = {
properties: [
{ id: 1, owner: { id: 1, name: 'Juan' } },
{ id: 2, owner: { id: 1, name: 'Juan' } }
]
};
// Usar entidades normalizadas
const goodState = {
entities: {
users: { 1: { id: 1, name: 'Juan' } },
properties: {
1: { id: 1, ownerId: 1 },
2: { id: 2, ownerId: 1 }
}
}
};
Selectores Memoizados
import { createSelector } from '@reduxjs/toolkit';
const selectProperties = (state: RootState) => state.properties;
const selectFilter = (state: RootState) => state.filter;
export const selectFilteredProperties = createSelector(
[selectProperties, selectFilter],
(properties, filter) => {
return properties.filter(property =>
property.title.toLowerCase().includes(filter.toLowerCase())
);
}
);