Building a Role-Based Dashboard in React with Just Context API and localStorage (No Backend)
I wanted a project that would force me to actually think about state management instead of just shipping another CRUD app with a database doing all the heavy lifting. So I built EMS — an admin/employee task management app where the entire persistence layer is localStorage. No Express server, no MongoDB, nothing. Just React figuring out who you are and what you can see.
The core problem: one app, two very different UIs
Admins and employees needed completely different dashboards, but I didn't want two separate apps or a mess of if (role === 'admin') checks scattered across components. So the role lives in a single AuthProvider that wraps the app, and routing/rendering decisions read off that context instead of prop-drilling a role flag through five layers of components.
src/ ├── context/ │ └── AuthProvider.jsx # login state, user role, task data — all global ├── components/ │ ├── Auth/ │ ├── Dashboard/ │ │ ├── AdminDashboard.jsx │ │ └── EmployeeDashboard.jsx │ └── TaskList/ ├── utils/ │ └── localStorage.js # read/write helpers └── App.jsx Once a user logs in, AdminDashboard and EmployeeDashboard are literally just different trees — no shared "mode" component trying to be clever about conditional rendering. Admin side creates and assigns tasks; employee side can accept, update, and mark tasks done/failed. Both read and write through the same task state, so an admin assigning a task and an employee updating its status are touching the same source of truth.
Why localStorage instead of a backend
Honestly — scope control. The point of this project was to get good at React's own state primitives, not to re-prove I can wire up an Express API (I've done that already, backend's usually Node/Express/MongoDB for me). localStorage.js is a thin wrapper that serializes the task list and user session on every mutation, so refreshing the page doesn't nuke your state. It's obviously not multi-device or multi-user safe — two people can't share a task list across browsers — but for a single-session demo of role-based architecture, it does the job without extra infra getting in the way.
What I'd actually change next
If I took this further I'd swap the localStorage layer for a real backend without touching the dashboard components at all — which is kind of the point of isolating persistence behind utils/localStorage.js in the first place. Swap the implementation, keep the interface.
Stack: React, Vite, Tailwind, Context API.
Live: ems.faheem.foo
Code: github.com/faheeemm/ems
Curious what people would do differently with the Context setup — feels like the kind of thing that works fine at this scale and falls apart the moment you add a third role.