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:

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
The key rule: a component in 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);
Context is not a state management library โ€” don't use it for frequently-updating state like form values or UI toggles. Use it for stable, app-wide data like the current user, theme, or locale.

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.