Why I stopped over-engineering state on elangodev.com
When I rebuilt my portfolio at elangodev.com, I had a familiar temptation: import a global store before I had a real problem. I had used Redux on client projects, Zustand in prototypes, and I kept reading about Signals as the next big thing. But my site is not a trading terminal — it is a public profile, a dashboard I use to edit content, blog and handbook pages, chat, and a blog. I needed a decision framework that matched how I actually ship features, not how conference talks describe ideal architectures.
After a year of production traffic, my rule is simple. I use React Context for slow-changing shared data, local useState for UI that belongs to one screen, and I only add a dedicated store when multiple distant components must react to high-frequency updates. That split keeps bundle size down and makes debugging on Vercel previews straightforward.
ProfileContext: the one global context I trust
My public homepage and several inner pages need the same profile row from Supabase — name, bio, photo, theme preference. Fetching that in every route would duplicate network work and flash inconsistent loading states. I wrapped the app in ProfileProvider inside app/layout.jsx so every client island can call useProfile().
The provider loads once on mount, exposes loading and error flags, and keeps the last good profile if a refresh fails during auth transitions. That resilience mattered when I wired Supabase session refresh on the dashboard: a transient 401 should not blank my hero section for visitors.
export const ProfileProvider = ({ children }) => {
const [profile, setProfile] = useState(null);
const [loading, setLoading] = useState(true);
const fetchProfile = async () => {
const { data } = await supabase
.from("users")
.select("*")
.eq("full_name", "elango")
.maybeSingle();
if (data) setProfile(data);
setLoading(false);
};
return (
<ProfileContext.Provider
value={{ profile, loading, refreshProfile: fetchProfile }}
>
{children}
</ProfileContext.Provider>
);
};HomeContent.jsx reads contextProfile and merges it with server props from app/page.jsx. That hybrid pattern is how I use React 19 with Next.js 16: the server fetches initialProfile for fast LCP, the client context catches updates after I edit my bio in the dashboard without a full redeploy.
Dashboard state stays local on purpose
My dashboard at /dashboard is the opposite story. It manages modals, chart datasets, active sidebar sections, and form drafts for experience rows, skills, blog posts, and freelance requests. Almost none of that belongs in a global store. When I open the experience editor, only DashboardContent cares about expEdit and expOpen.
I keep activeSection in useState("overview") and lazy-load heavy panels with next/dynamic — AnalyticsSection and BlogSection load with ssr: false so Chart.js does not block first paint. If I promoted every modal flag to Context, unrelated nav items would re-render when I toggle a skill form. Context updates propagate to all consumers; my dashboard has dozens of consumers in one file tree.
const AnalyticsSection = dynamic(
() => import("./AnalyticsSection"),
{ ssr: false }
);
const [activeSection, setActiveSection] = useState("overview");
const [expOpen, setExpOpen] = useState(false);
const [expEdit, setExpEdit] = useState(null);When I need profile data in the dashboard, I still call useProfile() — that is the boundary. Global identity, local editing UI. I have not needed Zustand here because the dashboard is one large client component with colocated state, not a sprawling multi-route admin app shared across teams.
Where Zustand and Signals would enter my stack
I evaluate Zustand when two conditions are true: state must survive route changes, and updates are too frequent for Context. A future example on elangodev.com might be a unified notification center spanning chat, freelance requests, and blog comments — three features that already touch Supabase realtime. A small store with selectors would limit re-renders better than lifting everything into ProfileContext.
Signals interest me for frontend experiments — canvas tools, drag coordinates, live sliders — where Framer Motion already handles visual interpolation but underlying numeric state changes every frame. I have not migrated production routes to Signals yet because React 19 concurrent rendering plus useSpring in HomeContent.jsx already gives me smooth scroll progress without a second reactivity model.
Practical checklist I use before adding a library
First, I ask whether the state is server data, UI ephemeral, or cross-cutting user session. Server data belongs in Supabase queries, cached by Next.js revalidate on public pages. UI ephemeral stays in useState. Session-like profile data fits Context with a narrow API — profile, loading, refreshProfile — not a grab bag of unrelated fields.
Second, I measure re-render pain in React DevTools before abstracting. My portfolio rarely needed that step. Third, I check bundle impact: Zustand is tiny, but every dependency on elangodev.com is scrutinized because AdSense and analytics already compete for main-thread budget.
Fourth, I document the boundary in code review for my future self. When I added chat push notifications, message list state stayed inside chat components; only auth identity flows through existing providers. That discipline prevents the "god context" anti-pattern I fought on older client projects.
What I would do differently on a larger product
If elangodev.com grew into a multi-tenant SaaS, I would split stores by domain: auth, billing, editor. Context would remain for theme and locale. Signals would back live collaboration in elangodev.com's code playground. For a personal portfolio and lab site, though, Context plus local state remains the sweet spot — less ceremony, easier to reason about when I deploy from my laptop at midnight.
State management in 2026 is not about picking the trendiest library. It is about matching update frequency and scope to the simplest tool that keeps elangodev.com fast and maintainable. I built ProfileContext because profile data is truly shared. I kept dashboard modals local because they are not. Everything else can wait until the profiler proves I need more.



