Skip to main content

Overview

MotorDesk uses custom React hooks to encapsulate business logic, state management, and side effects. This approach keeps components clean and makes logic reusable across the application.

Available Hooks

All custom hooks are located in src/hooks/:
hooks/
├── useAuth.ts          # Authentication and user session
├── useCustomers.ts     # Customer CRUD operations
├── useHomeStats.ts     # Dashboard statistics
├── useMainLayout.ts    # Layout state and navigation
├── usePermissions.ts   # Role-based access control
├── useProducts.ts      # Product management
├── useReports.ts       # Report generation
├── useSales.ts         # Sales and invoicing
├── useSettings.ts      # Application settings
└── useVehicles.ts      # Vehicle fleet management

Hook Categories

Authentication

useAuthUser login, registration, session management

Data Management

useCustomers, useVehicles, useProducts, useSalesCRUD operations for business entities

UI State

useMainLayoutLayout state, modals, navigation

Business Logic

usePermissions, useHomeStats, useReportsComplex business rules and calculations

Configuration

useSettingsApp configuration and preferences

useAuth Hook

Handles authentication, user session, and onboarding. Location: src/hooks/useAuth.ts

Return Values

interface UseAuthReturn {
  // State (from Redux)
  user: User | null;
  token: string | null;
  isAuthenticated: boolean;
  isLoading: boolean;
  error: string | null;
  needsOnboarding: boolean;
  
  // Form state
  email: string;
  setEmail: (email: string) => void;
  password: string;
  setPassword: (password: string) => void;
  
  // Actions
  handleLogin: (e?: React.FormEvent) => Promise<boolean>;
  register: (name: string, email: string, password?: string) => Promise<boolean>;
  logout: () => void;
  finishOnboarding: () => void;
}

Usage Example

import { useAuth } from '@hooks/useAuth';

export const Login = () => {
  const { 
    email, 
    setEmail, 
    password, 
    setPassword,
    handleLogin, 
    isLoading, 
    error 
  } = useAuth();

  return (
    <form onSubmit={handleLogin}>
      <Input 
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <Input 
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <Button type="submit" isLoading={isLoading}>
        Login
      </Button>
      {error && <p className="text-red-500">{error}</p>}
    </form>
  );
};

Key Features

Simulates authentication against mock user data:
const handleLogin = async (e?: React.FormEvent) => {
  if (e) e.preventDefault();
  
  dispatch(loginStart()); // Set loading state
  
  try {
    // Simulate network delay
    await new Promise(resolve => setTimeout(resolve, 1500));
    
    // Find user in mock data
    const userFound = usersData.users.find(u => u.email === email.toLowerCase());
    
    if (userFound && password === '123456') {
      dispatch(loginSuccess({
        user: userFound as User,
        isNew: false,
        token: `mock-jwt-token-para-${userFound.id}`
      }));
      return true;
    } else {
      dispatch(loginFailure('Credenciales incorrectas'));
      return false;
    }
  } catch (error) {
    dispatch(loginFailure('Error interno'));
    return false;
  }
};
Creates a new owner account (first-time setup):
const register = async (nombreNuevo: string, emailNuevo: string) => {
  dispatch(loginStart());
  
  const newUser: User = {
    id: `user-new-${Date.now()}`,
    branchIds: [],
    nombre: nombreNuevo,
    email: emailNuevo.toLowerCase(),
    rol: UserRole.OWNER, // Default role
  };
  
  dispatch(loginSuccess({
    user: newUser,
    isNew: true, // Triggers onboarding flow
    token: `mock-jwt-token-new-${newUser.id}`
  }));
};
Uses Redux for global auth state:
import { useDispatch, useSelector } from 'react-redux';
import { loginStart, loginSuccess, loginFailure, logout } from '@store/slices/authSlice';

const dispatch = useDispatch();
const authState = useSelector((state: RootState) => state.auth);

useCustomers Hook

Manages customer data with search, pagination, and modal state. Location: src/hooks/useCustomers.ts

Return Values

interface UseCustomersReturn {
  // Search and filtering
  searchTerm: string;
  handleSearch: (e: React.ChangeEvent<HTMLInputElement>) => void;
  
  // Pagination
  currentCustomers: Customer[];
  currentPage: number;
  totalPages: number;
  setCurrentPage: (page: number) => void;
  itemsPerPage: number;
  handleItemsPerPageChange: (e: React.ChangeEvent<HTMLSelectElement>) => void;
  totalItems: number;
  
  // Modal state
  activeModal: ModalType;
  openModal: (type: ModalType, customer?: any) => void;
  closeModal: () => void;
  selectedCustomer: any;
  
  // External API
  fetchDecolectaData: (tipoDoc: string, numeroDoc: string) => Promise<any>;
  isSearchingApi: boolean;
}

Usage Example

import { useCustomers } from '@hooks/useCustomers';

export const Customers = () => {
  const {
    searchTerm,
    handleSearch,
    currentCustomers,
    currentPage,
    totalPages,
    setCurrentPage,
    openModal,
    closeModal,
    activeModal,
    selectedCustomer,
  } = useCustomers();

  return (
    <div>
      <Input 
        placeholder="Search customers..."
        value={searchTerm}
        onChange={handleSearch}
      />
      
      {currentCustomers.map(customer => (
        <div key={customer.id}>
          <h3>{customer.nombreRazonSocial}</h3>
          <Button onClick={() => openModal('edit', customer)}>
            Edit
          </Button>
        </div>
      ))}
      
      <Pagination 
        currentPage={currentPage}
        totalPages={totalPages}
        onPageChange={setCurrentPage}
      />
      
      <CustomerFormModal 
        isOpen={activeModal === 'edit'}
        customer={selectedCustomer}
        onClose={closeModal}
      />
    </div>
  );
};

Key Features

Automatically calculates related vehicle counts:
const mappedCustomers = useMemo(() => {
  return db.customers.map(customer => {
    const vehiculosPropios = db.vehicles.filter(
      v => v.propietarioId === customer.id && !v.isDeleted
    );
    const vehiculosConducidos = db.vehicles.filter(
      v => v.conductorHabitualId === customer.id && !v.isDeleted
    );
    
    return {
      ...customer,
      vehiculosPropios,
      vehiculosConducidos,
      totalVehiculosRelacionados: vehiculosPropios.length + vehiculosConducidos.length
    };
  });
}, []);
Filters customers as user types:
const filteredCustomers = useMemo(() => {
  return mappedCustomers.filter((c) => {
    const term = searchTerm.toLowerCase();
    return (
      c.numeroDocumento.toLowerCase().includes(term) ||
      c.nombreRazonSocial.toLowerCase().includes(term) ||
      (c.email && c.email.toLowerCase().includes(term))
    );
  });
}, [mappedCustomers, searchTerm]);
Calculates current page items:
const totalPages = Math.ceil(filteredCustomers.length / itemsPerPage);

const currentCustomers = useMemo(() => {
  const startIndex = (currentPage - 1) * itemsPerPage;
  return filteredCustomers.slice(startIndex, startIndex + itemsPerPage);
}, [filteredCustomers, currentPage, itemsPerPage]);
Validates RUC/DNI against SUNAT/RENIEC:
const fetchDecolectaData = async (tipoDoc: string, numeroDoc: string) => {
  if (!numeroDoc) return null;
  
  setIsSearchingApi(true);
  
  // Simulate API call
  await new Promise(resolve => setTimeout(resolve, 1500));
  
  setIsSearchingApi(false);
  
  // Mock responses
  if (tipoDoc === 'DNI' && numeroDoc.length === 8) {
    return {
      nombreRazonSocial: "MARIO ALBERTO ROJAS PEREZ",
      direccion: "CALLE LOS PINOS 123, LIMA",
    };
  } else if (tipoDoc === 'RUC' && numeroDoc.length === 11) {
    return {
      nombreRazonSocial: "MINERA LOS ANDES S.A.A.",
      direccion: "AV. PRINCIPAL 456, AREQUIPA",
    };
  }
  
  return null;
};

Common Hook Patterns

All entity management hooks follow this pattern:
export type ModalType = 'details' | 'add' | 'edit' | 'delete' | null;

export const useEntity = () => {
  const [activeModal, setActiveModal] = useState<ModalType>(null);
  const [selectedItem, setSelectedItem] = useState<any>(null);
  
  const openModal = (type: ModalType, item?: any) => {
    setSelectedItem(item || null);
    setActiveModal(type);
  };
  
  const closeModal = () => {
    setActiveModal(null);
    setSelectedItem(null);
  };
  
  return { activeModal, selectedItem, openModal, closeModal };
};

Search and Filter Pattern

const [searchTerm, setSearchTerm] = useState("");

const filteredItems = useMemo(() => {
  return items.filter(item => 
    item.name.toLowerCase().includes(searchTerm.toLowerCase())
  );
}, [items, searchTerm]);

const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
  setSearchTerm(e.target.value);
  setCurrentPage(1); // Reset to first page on search
};

Pagination Pattern

const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);

const totalPages = Math.ceil(filteredItems.length / itemsPerPage);

const currentItems = useMemo(() => {
  const startIndex = (currentPage - 1) * itemsPerPage;
  return filteredItems.slice(startIndex, startIndex + itemsPerPage);
}, [filteredItems, currentPage, itemsPerPage]);

Best Practices

Single Responsibility

Each hook should have one clear purpose. Don’t create “god hooks” that do everything.

Memoization

Use useMemo for expensive calculations. Use useCallback for function references passed as props.

Dependency Arrays

Always specify correct dependencies in useEffect, useMemo, and useCallback.

Return Object

Return an object (not array) for hooks with multiple values. This allows selective destructuring.

Type Safety

Always define TypeScript interfaces for hook parameters and return values.

Naming Convention

Always prefix with use (e.g., useAuth, useCustomers). This is required by React’s rules.

Hook Dependencies

Redux Integration

Many hooks integrate with Redux for global state:
import { useDispatch, useSelector } from 'react-redux';
import { type RootState } from '@/store';

const dispatch = useDispatch();
const authState = useSelector((state: RootState) => state.auth);

Local Database (Mock)

Hooks access the mock database for CRUD operations:
import { db } from '@data/db';

const customers = db.customers.filter(c => !c.isDeleted);

Testing Hooks

Use React Testing Library’s renderHook to test custom hooks:
import { renderHook, act } from '@testing-library/react';
import { useCustomers } from './useCustomers';

describe('useCustomers', () => {
  it('filters customers by search term', () => {
    const { result } = renderHook(() => useCustomers());
    
    act(() => {
      result.current.handleSearch({ target: { value: 'John' } });
    });
    
    expect(result.current.searchTerm).toBe('John');
    expect(result.current.currentPage).toBe(1); // Reset to first page
  });
  
  it('opens and closes modals', () => {
    const { result } = renderHook(() => useCustomers());
    
    act(() => {
      result.current.openModal('add');
    });
    
    expect(result.current.activeModal).toBe('add');
    expect(result.current.selectedCustomer).toBeNull();
    
    act(() => {
      result.current.closeModal();
    });
    
    expect(result.current.activeModal).toBeNull();
  });
});

Performance Optimization

1

Memoize Expensive Calculations

Use useMemo for filtering, sorting, and transformations:
const sortedItems = useMemo(() => {
  return items.sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
2

Stabilize Callbacks

Use useCallback for event handlers passed to child components:
const handleClick = useCallback((id: string) => {
  setSelectedId(id);
}, []);
3

Avoid Unnecessary Re-renders

Only include necessary dependencies in dependency arrays:
// Bad: Re-runs on every render
useEffect(() => {
  fetchData();
});

// Good: Runs only when id changes
useEffect(() => {
  fetchData(id);
}, [id]);
4

Debounce Search Input

For search inputs, consider debouncing:
const [searchTerm, setSearchTerm] = useState('');
const [debouncedSearch, setDebouncedSearch] = useState('');

useEffect(() => {
  const timer = setTimeout(() => {
    setDebouncedSearch(searchTerm);
  }, 300);
  
  return () => clearTimeout(timer);
}, [searchTerm]);

Hook Composition

Hooks can use other hooks:
export const useVehicleForm = () => {
  const { user } = useAuth(); // Use auth hook
  const { customers } = useCustomers(); // Use customers hook
  
  const [selectedOwner, setSelectedOwner] = useState(null);
  
  // Only show customers accessible to this user
  const availableOwners = useMemo(() => {
    return customers.filter(c => 
      user?.branchIds.includes(c.branchId)
    );
  }, [customers, user]);
  
  return { availableOwners, selectedOwner, setSelectedOwner };
};

Next Steps

Build docs developers (and LLMs) love