Overview
MotorDesk uses Redux Toolkit for state management and Redux Saga for handling asynchronous side effects. This combination provides predictable state updates with powerful async operation handling.
Technology Stack
Redux Toolkit Modern Redux with less boilerplate and built-in best practices
Redux Saga Generator-based side effect management for complex async flows
Store Configuration
Store Setup
The Redux store is configured in src/store/index.ts with persistence and saga middleware:
// src/store/index.ts:1-8
import { configureStore } from '@reduxjs/toolkit' ;
import createSagaMiddleware from 'redux-saga' ;
import { persistStore , persistReducer } from 'redux-persist' ;
import localforage from 'localforage' ;
import { combineReducers } from 'redux' ;
import authReducer from './slices/authSlice' ;
import salesReducer from './slices/salesSlice' ;
import rootSaga from './rootSaga' ;
Root Reducer
The application combines multiple slice reducers into a root reducer:
// src/store/index.ts:21-24
const rootReducer = combineReducers ({
auth: authReducer ,
sales: salesReducer ,
});
Currently, the application has two main state slices: auth for authentication and sales for sales/billing operations.
Store Creation
// src/store/index.ts:26-38
const persistedReducer = persistReducer ( persistConfig , rootReducer );
const sagaMiddleware = createSagaMiddleware ();
export const store = configureStore ({
reducer: persistedReducer ,
middleware : ( getDefaultMiddleware ) =>
getDefaultMiddleware ({
thunk: false ,
serializableCheck: {
ignoredActions: [ 'persist/PERSIST' , 'persist/REHYDRATE' , 'persist/REGISTER' ],
},
}). concat ( sagaMiddleware ),
});
sagaMiddleware . run ( rootSaga );
Redux Thunk is explicitly disabled (thunk: false) since Redux Saga handles all async operations.
Type Definitions
Root State and Dispatch Types
The store exports TypeScript types for type-safe state access:
// src/store/index.ts:44-45
export type RootState = ReturnType < typeof store . getState >;
export type AppDispatch = typeof store . dispatch ;
Use these types to create typed hooks:
import { TypedUseSelectorHook , useDispatch , useSelector } from 'react-redux' ;
import type { RootState , AppDispatch } from './store' ;
export const useAppDispatch = () => useDispatch < AppDispatch >();
export const useAppSelector : TypedUseSelectorHook < RootState > = useSelector ;
Redux Slices
Auth Slice
Manages authentication state and user information:
// src/store/slices/authSlice.ts:4-19
export interface User {
id : string ;
branchIds : string [];
nombre : string ;
email : string ;
rol : UserRole ;
}
interface AuthState {
user : User | null ;
token : string | null ;
isAuthenticated : boolean ;
isLoading : boolean ;
error : string | null ;
needsOnboarding : boolean ;
}
Auth Actions
Sets loading state when login begins loginStart : ( state ) => {
state . isLoading = true ;
state . error = null ;
}
Stores user data and token after successful authentication loginSuccess : ( state , action : PayloadAction <{ user : User ; isNew ?: boolean ; token : string }>) => {
state . isLoading = false ;
state . isAuthenticated = true ;
state . user = action . payload . user ;
state . token = action . payload . token ;
state . needsOnboarding = action . payload . isNew || false ;
state . error = null ;
}
Records error message when login fails loginFailure : ( state , action : PayloadAction < string >) => {
state . isLoading = false ;
state . error = action . payload ;
}
Clears all authentication state logout : ( state ) => {
state . user = null ;
state . token = null ;
state . isAuthenticated = false ;
state . needsOnboarding = false ;
}
Marks onboarding as complete for new users completeOnboarding : ( state ) => {
state . needsOnboarding = false ;
}
Usage Example
import { useAppDispatch , useAppSelector } from '@/hooks/redux' ;
import { loginStart , loginSuccess , loginFailure } from '@/store/slices/authSlice' ;
function LoginComponent () {
const dispatch = useAppDispatch ();
const { isLoading , error } = useAppSelector ( state => state . auth );
const handleLogin = async ( email : string , password : string ) => {
dispatch ( loginStart ());
try {
const response = await api . login ( email , password );
dispatch ( loginSuccess ({
user: response . user ,
token: response . token ,
isNew: response . isNew
}));
} catch ( err ) {
dispatch ( loginFailure ( err . message ));
}
};
}
Sales Slice
Manages sales data with offline sync support:
// src/store/slices/salesSlice.ts:3-12
interface Sale {
id : string ;
clienteId : string ;
montoTotal : number ;
sync_status : 'PENDING' | 'SYNCED' ;
}
interface SalesState {
items : Sale [];
}
Sales Actions
Adds a new sale with PENDING sync status addSaleRequest : ( state , action : PayloadAction < Omit < Sale , 'sync_status' >>) => {
state . items . push ({ ... action . payload , sync_status: 'PENDING' });
}
This action triggers the handleAddSale saga for background synchronization.
Updates the sync status of a specific sale updateSaleStatus : ( state , action : PayloadAction <{ id : string ; status : 'SYNCED' | 'PENDING' }>) => {
const sale = state . items . find ( item => item . id === action . payload . id );
if ( sale ) {
sale . sync_status = action . payload . status ;
}
}
Marks a sale as successfully synced addSaleSuccess : ( state , action : PayloadAction < string >) => {
const sale = state . items . find ( item => item . id === action . payload );
if ( sale ) sale . sync_status = 'SYNCED' ;
}
Usage Example
import { useAppDispatch , useAppSelector } from '@/hooks/redux' ;
import { addSaleRequest } from '@/store/slices/salesSlice' ;
import { v4 as uuidv4 } from 'uuid' ;
function CreateSaleForm () {
const dispatch = useAppDispatch ();
const sales = useAppSelector ( state => state . sales . items );
const handleSubmit = ( clienteId : string , montoTotal : number ) => {
dispatch ( addSaleRequest ({
id: uuidv4 (),
clienteId ,
montoTotal
}));
};
// Show pending sales with indicator
const pendingSales = sales . filter ( s => s . sync_status === 'PENDING' );
}
Redux Saga Implementation
Root Saga
The root saga coordinates all side effects in the application:
// src/store/rootSaga.ts:29-34
export default function* rootSaga () {
yield all ([
takeEvery ( addSaleRequest . type , handleAddSale ),
takeEvery ( 'app/RECONNECTED' , syncPendingSales )
]);
}
Saga Patterns
takeEvery: Runs a saga for every dispatched action (concurrent)
takeLatest: Cancels previous saga if new action arrives
takeLeading: Ignores new actions while saga is running
Sale Creation Saga
Handles online synchronization when a sale is created:
// src/store/rootSaga.ts:22-27
function* handleAddSale ( action : any ) {
if ( navigator . onLine ) {
yield delay ( 500 );
yield put ( addSaleSuccess ( action . payload . id ));
}
}
Check Network
Verify if the device is online using navigator.onLine
Delay
Wait 500ms to simulate API call (replace with actual API call)
Success Action
Dispatch addSaleSuccess to update sync status
Pending Sales Sync Saga
Synchronizes all pending sales when the app reconnects:
// src/store/rootSaga.ts:5-20
const getPendingSales = ( state : RootState ) =>
state . sales . items . filter (( item ) => item . sync_status === 'PENDING' );
function* syncPendingSales () {
if ( ! navigator . onLine ) return ;
const pendingSales : ReturnType < typeof getPendingSales > = yield select ( getPendingSales );
for ( const sale of pendingSales ) {
try {
yield delay ( 1000 );
yield put ( updateSaleStatus ({ id: sale . id , status: 'SYNCED' }));
} catch ( error ) {
console . error ( 'Error sincronizando venta:' , sale . id );
}
}
}
Saga Effects Reference
put Dispatches an action to the store yield put ( addSaleSuccess ( id ));
select Reads data from the Redux store const sales = yield select ( getPendingSales );
call Calls a function (typically async) const response = yield call ( api . createSale , data );
delay Pauses saga execution for specified milliseconds yield delay ( 1000 ); // Wait 1 second
takeEvery Spawns a saga on every action yield takeEvery ( ACTION_TYPE , handlerSaga );
all Runs multiple effects in parallel yield all ([ saga1 (), saga2 ()]);
Data Flow Diagram
Advanced Patterns
Selector Functions
Create reusable selectors for derived state:
// Selector for pending sales
const getPendingSales = ( state : RootState ) =>
state . sales . items . filter (( item ) => item . sync_status === 'PENDING' );
// Selector for synced sales
const getSyncedSales = ( state : RootState ) =>
state . sales . items . filter (( item ) => item . sync_status === 'SYNCED' );
// Usage in component
const pendingSales = useAppSelector ( getPendingSales );
Error Handling in Sagas
Always wrap saga logic in try-catch blocks:
function* handleCreateSale ( action : PayloadAction < Sale >) {
try {
const response = yield call ( api . createSale , action . payload );
yield put ( addSaleSuccess ( response . id ));
} catch ( error ) {
yield put ( addSaleFailure ({
id: action . payload . id ,
error: error . message
}));
console . error ( 'Sale creation failed:' , error );
}
}
Testing Sagas
Redux Saga provides testing utilities:
import { select , put , delay } from 'redux-saga/effects' ;
import { syncPendingSales } from './rootSaga' ;
test ( 'syncPendingSales handles pending sales' , () => {
const gen = syncPendingSales ();
// Test selector call
expect ( gen . next (). value ). toEqual ( select ( getPendingSales ));
// Test with mock pending sales
const mockSales = [{ id: '1' , sync_status: 'PENDING' }];
expect ( gen . next ( mockSales ). value ). toEqual ( delay ( 1000 ));
// Test dispatch
expect ( gen . next (). value ). toEqual (
put ( updateSaleStatus ({ id: '1' , status: 'SYNCED' }))
);
});
Best Practices
Reducers should never have side effects. All async operations belong in sagas, not reducers.
Leverage TypeScript for type-safe actions, state, and selectors. Define interfaces for all state shapes.
For complex data, normalize state shape using entities and IDs rather than nested objects.
Only persist slices that need to survive page refreshes. Don’t persist temporary UI state.
Always handle errors in sagas gracefully. Log errors and update state to reflect error conditions.
Use consistent action naming: addSaleRequest, addSaleSuccess, addSaleFailure.
Memoized Selectors
For expensive computations, use memoized selectors with Reselect:
import { createSelector } from '@reduxjs/toolkit' ;
const selectSales = ( state : RootState ) => state . sales . items ;
const selectPendingSalesCount = createSelector (
[ selectSales ],
( sales ) => sales . filter ( s => s . sync_status === 'PENDING' ). length
);
Saga Throttling
Limit how often sagas run using throttle:
import { throttle } from 'redux-saga/effects' ;
function* rootSaga () {
yield throttle ( 5000 , 'app/SYNC_REQUEST' , syncPendingSales );
}
This runs syncPendingSales at most once every 5 seconds, even if multiple app/SYNC_REQUEST actions are dispatched.
Debugging
The store is automatically configured to work with Redux DevTools:
Install Redux DevTools browser extension
Open DevTools in your browser
Navigate to Redux tab
Inspect actions, state, and time-travel debug
Saga Monitor
Enable saga monitoring in development:
import createSagaMiddleware from 'redux-saga' ;
import sagaMonitor from '@redux-saga/simple-saga-monitor' ;
const sagaMiddleware = createSagaMiddleware ({
sagaMonitor: process . env . NODE_ENV === 'development' ? sagaMonitor : undefined
});
Next Steps
Offline Sync Learn how state persistence enables offline functionality
Architecture See how state management fits into the overall architecture