When I started the GeriCare Analytics Dashboard, the codebase was a single 3,100-line Dashboard component. Everything โ data fetching, state management, UI logic, utility functions โ lived in one file. Adding a feature meant scrolling through thousands of lines. Fixing a bug in one section risked breaking another.
Refactoring it into a maintainable structure was one of the most valuable things I did on that project. Here's the approach I landed on and the reasoning behind each decision.
The problem with the monolith
A large component file isn't just uncomfortable to work with โ it creates concrete problems:
- React re-renders the entire component tree on any state change
- You can't test individual pieces in isolation
- Two developers can't work on the same component without constant merge conflicts
- A bug in one section can mask bugs in unrelated sections
The 3,100-line Dashboard had all of these problems. State updates in the WhatsApp monitor section were triggering re-renders of the finance charts. It was slow and unpredictable.
The folder structure I use
src/
components/
ui/ โ reusable, no business logic
Button.jsx
Card.jsx
Modal.jsx
features/ โ one folder per feature
whatsapp/
WhatsappMonitor.jsx
useWhatsappData.js
whatsapp.utils.js
finance/
FinanceOverview.jsx
useFinanceData.js
campaigns/
CampaignManager.jsx
useCampaigns.js
hooks/ โ shared custom hooks
useAuth.js
useFirestore.js
context/ โ React context providers
AuthContext.jsx
utils/ โ pure utility functions
formatCurrency.js
formatDate.js
pages/ โ route-level components
Dashboard.jsx
Reports.jsx
ui/ should never import from features/. It should only receive data via props. This keeps your building blocks genuinely reusable.Feature-based folders
The most impactful change was grouping by feature rather than by file type. Compare:
By type (common, harder to maintain):
components/WhatsappMonitor.jsx
hooks/useWhatsappData.js
utils/whatsapp.utils.js
By feature (what I use):
features/whatsapp/
WhatsappMonitor.jsx
useWhatsappData.js
whatsapp.utils.js
When you need to work on the WhatsApp feature, everything related to it is in one place. When you delete it, you delete one folder. No hunting across the codebase.
Custom hooks for data fetching
Moving data fetching into custom hooks was the single change that improved code quality the most. Before:
// โ Data fetching mixed with UI code
function FinanceOverview() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// 40 lines of fetch logic here
}, []);
return <div>...</div>;
}
After:
// โ
Clean separation
// hooks/useFinanceData.js
export function useFinanceData(centreId) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// all fetch logic here
}, [centreId]);
return { data, loading, error };
}
// FinanceOverview.jsx โ just UI
function FinanceOverview({ centreId }) {
const { data, loading, error } = useFinanceData(centreId);
if (loading) return <Spinner />;
if (error) return <ErrorCard error={error} />;
return <div>...</div>;
}
AuthContext โ one source of truth for user state
Before the refactor, the current user object was passed as a prop 4โ5 levels deep. Extracting it into AuthContext eliminated the prop drilling entirely:
// context/AuthContext.jsx
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
useEffect(() => {
return onAuthStateChanged(auth, setUser);
}, []);
return (
<AuthContext.Provider value={{ user, setUser }}>
{children}
</AuthContext.Provider>
);
}
// any component, anywhere in the tree:
const { user } = useContext(AuthContext);
What this actually changed
After the refactor, the Dashboard component went from 3,100 lines to 280. It became a layout file that imported feature components. Adding a new feature meant creating a new folder โ nothing else changed. The re-render performance issue disappeared because each feature component managed its own state.
Good React structure isn't about following a pattern because it's popular. It's about organising code so that the next change โ adding a feature, fixing a bug, onboarding someone new โ takes minutes instead of hours.