Imagine our initial sessions-container.tsx
file was like a star player trying to do everything on the court. They're:
- Managing all the plays (session state)
- Coordinating with every teammate (trainee data)
- Calling all the shots (instructor assignments)
- Even handling ticket sales (UI rendering)
That's exhausting and prone to mistakes!
// Before: One star player doing everything
const SessionsContainer = () => {
const [trainees, setTrainees] = useState([]);
const [instructors, setInstructors] = useState([]);
const [sessions, setSessions] = useState([]);
// 700+ lines of mixed responsibilities!
};
// After: Specialized players with clear roles
const SessionsContainer = () => {
const { trainees } = useTrainees(); // Point Guard
const { instructors } = useInstructors(); // Shooting Guard
const { sessions } = useSessions(); // Center
// Now just ~300 lines focusing on coordination!
};
export function useTrainees() {
const [traineeIdToTrainee, setTraineeIdToTrainee] = useState({});
// Handles trainee data management
return { traineeIdToTrainee };
}
export function useInstructors() {
const [instructors, setInstructors] = useState([]);
// Manages instructor assignments
return { instructors };
}
export function useSessions(currentRoom: string) {
const [sessions, setSessions] = useState([]);
const removeSession = async (session) => {
/* ... */
};
// Core session management logic
return { sessions, removeSession };
}
Just like how a point guard focuses on playmaking:
// Each hook has ONE clear purpose
function useTrainees() {
// Only trainee-related logic lives here
}
Like keeping the ball with your best handler, custom hooks help us manage state better in several ways:
// File: src/hooks/useSessions.ts
function useSessions() {
// State and its related operations live together
const [sessions, setSessions] = useState([]);
const [dailyHourToSessions, setDailyHourToSessions] = useState({});
// Operations that modify this state live here too
const removeSession = async (session) => {
await sessionsService.remove(session.id);
setDailyHourToSessions((prev) => /* update logic */);
};
return { sessions, dailyHourToSessions, removeSession };
}
// File: src/pages/sessions-container.tsx
// Before: Component had to know how to update state
const SessionsContainer = () => {
const [sessions, setSessions] = useState([]);
const removeSession = async (session) => {
await sessionsService.remove(session.id);
setSessions(/* complex update logic */);
};
};
// After: Component just calls the hook's method
const SessionsContainer = () => {
const { removeSession } = useSessions();
// Component doesn't need to know HOW the state is updated
};
// File: src/hooks/useSessions.ts
function useSessions() {
// All state updates for sessions happen in one place
// This prevents inconsistent state updates across components
const updateSession = (session) => {
setSessions(/* ... */);
setDailyHourToSessions(/* ... */);
// Any other related state updates
};
}
// Files: src/pages/sessions-container.tsx & src/components/session-card/SessionCard.tsx
// Multiple components can use the same state management
const SessionsPage = () => {
const { sessions } = useSessions();
};
const SessionCard = () => {
const { sessions } = useSessions();
};
This approach:
- Reduces bugs from inconsistent state updates
- Makes state changes more predictable
- Simplifies component logic
- Makes testing easier since state logic is isolated
Like practicing specific plays:
test("useTrainees hook", () => {
const { result } = renderHook(() => useTrainees());
expect(result.current.traineeIdToTrainee).toBeDefined();
});
Here's how state flows in our refactored application, using useSessions
as an example:
graph TD
subgraph "Data Layer"
subgraph "Services"
SessionService[sessions.ts]
end
subgraph "Types"
SessionTypes[session.ts]
end
end
subgraph "State Management"
subgraph "Hooks"
SessionsHook[useSessions.ts]
end
end
subgraph "UI Layer"
Container[sessions-container.tsx]
Card[SessionCard.tsx]
end
SessionService --> SessionsHook
SessionTypes --> SessionsHook
SessionsHook --> Container
Container --> Card
Card --> SessionsHook
- Types define the shape of our data
- Services handle API communication
- Hooks manage state and operations
- Components use state and trigger actions
classDiagram
class Session {
+id: string
+traineeId: string
+datetime: string
+categories: Record
+comments: string
}
class Trainee {
+id: string
+name: string
+nextSession: Session
}
class Instructor {
+id: string
+name: string
}
class SessionService {
+getAll(): Promise~Session[]~
+remove(id: string): Promise~void~
+save(session: Session): Promise~Session~
}
class useSessions {
-sessions: Session[]
-dailyHourToSessions: Record
-newSessionsSummary: SessionForm[]
-isLoading: boolean
+fetchSessions(): Promise~void~
+removeSession(session: Session): Promise~void~
+getDailyHourToSessions(date: string, hour: string): Session[]
+clearNextSession(traineeId: string): Promise~void~
}
class SessionsContainer {
-selectedHour: string
-selectedDate: string
-currentRoom: string
+addCard(): void
+onSaveSession(session: Session): Promise~void~
+updateFormField(field: string, value: any): void
}
class SessionCard {
+trainee: Trainee
+instructor: Instructor
+date: string
+hour: string
+categories: Record
+exercises: string[]
+onSaveSession(): Promise~void~
+removeSession(): Promise~void~
+onCategoryChange(key: string, value: any): void
}
SessionsContainer --> SessionCard
SessionsContainer ..> useSessions
SessionCard ..> useSessions
useSessions --> SessionService
useSessions ..> Session
SessionCard ..> Session
SessionCard ..> Trainee
SessionCard ..> Instructor
-
Code Size Reduction
- sessions-container.tsx: 700+ β ~300 lines
- Clearer responsibility boundaries
- Easier to maintain
-
Better Error Handling
- Each hook manages its own errors
- Clear error boundaries
- Like each player knowing their recovery moves
-
Improved Developer Experience
- Easier to understand
- Easier to modify
- Clear separation of concerns
-
Further Hook Extraction
- Create useSessionForm for form logic
- Extract validation logic
- Like developing specialized plays
-
Add Error Boundaries
- Implement React Error Boundaries
- Add fallback UI
- Like having backup plays ready
Just as a basketball team needs different players with specialized skills:
- Don't make one component do everything
- Use custom hooks for specific logic
- Keep components focused on their primary role
- Test each piece independently
The key is to identify distinct responsibilities and create focused hooks that handle them well - just like how each basketball player masters their position!
- Previous: Data Structures
- Next: Trainee Availability
- Back to Home