Upgrading elangodev.com to React 19 and Next.js 16
I run elangodev.com on Next.js 16.1 with React 19.2. The upgrade was not about chasing version numbers — I wanted clearer boundaries between server data fetching and client interactivity. My homepage, blog index, and blog post pages fetch from Supabase on the server. Dashboard, chat, elangodev.com, and Framer Motion animations stay client-side. React 19 formalizes patterns I was already approximating with the App Router.
The mental model I use daily: Server Components render once per request (or revalidation window) and never ship event handlers to the browser. Client Components — marked with "use client" — hydrate where state, effects, and browser APIs are required. Mixing them incorrectly causes hydration warnings or bloated JavaScript bundles.
Root layout: server shell, client providers
app/layout.jsx is a Server Component. It exports metadata with metadataBase https://elangodev.com, Open Graph images, and Google verification when configured. Structured data for Person and WebSite schemas lives in the head without client JavaScript.
Inside body, I nest ThemeProvider and ProfileProvider — both client contexts — then Suspense-wrap analytics and ads. That Suspense boundary matters: AnalyticsTracker and AdSenseScript read consent from localStorage, which only exists in the browser.
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider>
<ProfileProvider>
<Suspense fallback={null}>
<AnalyticsTracker />
<GoogleAnalytics />
<AdSenseScript />
</Suspense>
{children}
<CookieConsent />
</ProfileProvider>
</ThemeProvider>
</body>
</html>
);
}Homepage: server fetch, client presentation
app/page.jsx is async. It calls createSimpleClient(), loads my users row with nested experience, education, and skills, and passes the result as props to HomeContent. export const revalidate = 3600 caches the page for an hour — a tradeoff I accept because profile edits are infrequent compared to anonymous visits.
HomeContent.jsx is "use client" because it uses Framer Motion scroll progress, section scrolling, and dynamic imports for below-the-fold sections. The server gives me fast HTML with real names and job titles; the client enhances with motion without blocking first contentful paint on a monolithic bundle.
const PublicNav = dynamic(() => import("../components/PublicNav"), { ssr: true });
const AboutSection = dynamic(() => import("./components/home/AboutSection"), { ssr: true });
const { scrollYProgress } = useScroll();
const scaleX = useSpring(scrollYProgress, {
stiffness: 100,
damping: 30,
restDelta: 0.001,
});I set ssr: true on homepage sections so crawlers and no-JS clients still see About and Experience markup. Heavier lab widgets are not on the homepage — that discipline keeps my React 19 server/client split intentional rather than defaulting everything to client.
Blog routes: generateStaticParams and metadata
app/blog/[slug]/page.jsx demonstrates server-only data access at scale. generateStaticParams queries published slugs from posts. generateMetadata calls getPostBySlug for unique titles and OG tags per article. The page component fetches post, profile, and recent posts in parallel with Promise.all — no client waterfall on first load.
PostDetailContent.jsx remains a client component for save-to-localStorage, share buttons, and fallback fetching when someone navigates client-side without SSR props. React 19 does not eliminate client components; it lets me choose them narrowly.
Server Actions versus Route Handlers on my site
I use Route Handlers — app/api/**/route.js — for chat, S3 uploads, and webhooks where I need explicit HTTP semantics, CORS, or third-party SDKs like web-push and AWS S3. Server Actions shine for form mutations colocated with UI; my dashboard still uses Supabase client calls from the browser for rapid iteration, but I am migrating write paths that touch secrets to server-only code.
When I adopt Server Actions for blog publishing, I will mark functions with "use server" inside server modules and call them from dashboard forms. React 19 serializes FormData automatically — less boilerplate than manual fetch with JSON bodies and CSRF considerations on every form.
Streaming, Suspense, and perceived performance
React 19 concurrent features pair naturally with Next.js loading.js files I use on slower lab routes. On the main portfolio, I prefer passing initial data from page.jsx so the hero renders in one server round trip. Suspense around analytics prevents consent-gated scripts from blocking children — a pattern I copied after measuring INP on mobile.
use() for promises is available in React 19, but I have not replaced every async server loader with client use() hooks. For elangodev.com, keeping async page components readable wins over demonstrating every new API in production.
What I tell other developers adopting React 19
Audit your tree from layout downward. Mark client boundaries at the lowest node that needs hooks. Push Supabase reads to server pages when rows are public — blog posts, profile highlights. Keep auth-gated dashboards client-first until Server Actions cover mutations.
Measure bundle size after removing accidental "use client" from leaf components that only render static markup. I cut kilobytes from elangodev.com entry points by splitting editors from marketing wrappers.
React 19 on elangodev.com is not a rewrite — it is a sharper split. Server Components deliver SEO and speed; client components deliver motion, chat, and the dashboard I use daily. That balance is the deep dive worth documenting: real URLs, real layout.jsx imports, and dynamic HomeContent.jsx imports I rely on every deploy.
When I onboard contributors to my repo, the first file I point them to is app/page.jsx paired with HomeContent.jsx — server fetch on top, client enhancement below. That pattern repeats on blog routes and will guide how I add Server Actions to dashboard forms next. React 19 did not change my product goals; it gave me clearer vocabulary for decisions I was already making in production.



