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.
How state is actually split on elangodev.com
ProfileContext loads author profile, skills, and social links for public chrome. Dashboard local UI — which analytics tab is open, draft blog form dirty state — stays in component state or a tiny Zustand store that never hydrates on the marketing site. Mixing those concerns into one global store made early builds re-render PublicNav on every keystroke in the admin editor.
My rule in 2026: Context for rarely changing server-fetched identity, Zustand for cross-widget client sessions inside /dashboard, and plain useState for anything a single tree owns. Signals-style libraries only enter the conversation when I profile a hot path with React DevTools and see Context cascading.
Case study: Featured posts vs profile theme
Theme toggles used to live next to profile data. Flipping dark mode re-rendered the entire home tree including featured article cards. I moved theme to ThemeContext with a narrow consumer on layout chrome only. FeaturedPosts now receives posts as props from a server loader — no client store required — which also helps AdSense crawlers see article titles in the initial HTML.
// Prefer server props for public content
// HomeContentLoader → getFeaturedPosts(3) → <FeaturedPosts posts={...} />
// Keep Zustand behind /dashboard auth gatesWhen Context is still the right call
Auth session from Supabase SSR helpers is inherently tree-wide. Duplicating session fetches in every leaf wastes round trips. I wrap ClientProviders once, pass a stable user object, and avoid putting ephemeral form state into that same provider. The failure mode to avoid is “god context” that updates on every mouse move.
Decision checklist I keep in the handbook
- Does this data come from the server for SEO? Prefer props / RSC.
- Do two distant client widgets share ephemeral UI state? Consider Zustand.
- Is it identity or theme read by many leaves but written rarely? Context is fine.
- Are you optimizing a canvas or editor hot path? Profile before adopting signals.
Most “state management debates” on this site ended with deleting stores, not adding them. The 2026 default on elangodev.com is boring on purpose.
Anti-patterns I removed during AdSense recovery
Homepage used to pull experience and education into the first paint for a resume aesthetic. That data still exists on /about, but the home tree no longer depends on those queries for the primary story. Fewer client contexts means fewer excuses for a thin marketing shell.
Another anti-pattern: storing draft blog HTML in Context shared with PublicNav. One mistyped dispatch blanked navigation labels. Draft editors now own their state. Publication pages stay read-mostly.
Testing state boundaries
I smoke-test by loading /, /blog, and /dashboard in separate sessions. Marketing routes must not download dashboard chunks. Route-based code splitting plus disciplined stores keeps the content site feeling like a publisher, which is exactly the signal AdSense reviewers need.
Summary for practitioners
If you take one idea from how elangodev.com is wired in 2026: put server content in props, put identity in a narrow context, put dashboard ephemera in a local store, and leave signals for measured hot paths. That hierarchy produced fewer bugs than any “one library to rule them all” rewrite I tried earlier in the decade.
Re-read your provider tree after every feature. If PublicNav re-renders when a dashboard chart updates, you still have a boundary problem — fix the boundary before adding memoization.





