Skip to main content

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));
  }
}
1

Check Network

Verify if the device is online using navigator.onLine
2

Delay

Wait 500ms to simulate API call (replace with actual API call)
3

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.

Performance Optimization

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

Redux DevTools

The store is automatically configured to work with Redux DevTools:
  1. Install Redux DevTools browser extension
  2. Open DevTools in your browser
  3. Navigate to Redux tab
  4. 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

Build docs developers (and LLMs) love