From 7ef0411ffa10f86e80512982ce8a8d7cfdf8778a Mon Sep 17 00:00:00 2001 From: danijel-lf Date: Tue, 26 May 2026 15:57:38 +0200 Subject: [PATCH] refactor: split feature UI into .server/.client component pairs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Server components (.server.tsx) resolve controllers from DI, prefetch data, and wrap client components in HydrationBoundary. Client components (.client.tsx) use hooks for hydration + background refetch. Barrel exports server components under clean names — consumers never see the server/client split. --- ...e-detail.tsx => article-detail.client.tsx} | 0 .../ui/components/article-detail.server.tsx | 24 +++++++++++++ ...ticle-list.tsx => article-list.client.tsx} | 0 .../src/ui/components/article-list.server.tsx | 24 +++++++++++++ packages/blog/src/ui/index.ts | 7 ++-- ...ge-content.tsx => page-content.client.tsx} | 0 .../src/ui/components/page-content.server.tsx | 35 +++++++++++++++++++ packages/marketing-pages/src/ui/index.ts | 2 +- ...site-header.tsx => site-header.client.tsx} | 0 .../src/ui/components/site-header.server.tsx | 27 ++++++++++++++ packages/navigation/src/ui/index.ts | 2 +- 11 files changed, 114 insertions(+), 7 deletions(-) rename packages/blog/src/ui/components/{article-detail.tsx => article-detail.client.tsx} (100%) create mode 100644 packages/blog/src/ui/components/article-detail.server.tsx rename packages/blog/src/ui/components/{article-list.tsx => article-list.client.tsx} (100%) create mode 100644 packages/blog/src/ui/components/article-list.server.tsx rename packages/marketing-pages/src/ui/components/{page-content.tsx => page-content.client.tsx} (100%) create mode 100644 packages/marketing-pages/src/ui/components/page-content.server.tsx rename packages/navigation/src/ui/components/{site-header.tsx => site-header.client.tsx} (100%) create mode 100644 packages/navigation/src/ui/components/site-header.server.tsx diff --git a/packages/blog/src/ui/components/article-detail.tsx b/packages/blog/src/ui/components/article-detail.client.tsx similarity index 100% rename from packages/blog/src/ui/components/article-detail.tsx rename to packages/blog/src/ui/components/article-detail.client.tsx diff --git a/packages/blog/src/ui/components/article-detail.server.tsx b/packages/blog/src/ui/components/article-detail.server.tsx new file mode 100644 index 0000000..7d08031 --- /dev/null +++ b/packages/blog/src/ui/components/article-detail.server.tsx @@ -0,0 +1,24 @@ +import { dehydrate, HydrationBoundary } from "@tanstack/react-query"; +import { getQueryClient } from "@repo/core-trpc"; +import { blogContainer } from "../../di/container"; +import { BLOG_SYMBOLS } from "../../di/symbols"; +import type { IGetArticleBySlugController } from "../../interface-adapters/controllers/get-article-by-slug.controller"; +import { ArticleDetail as ArticleDetailClient } from "./article-detail.client"; + +export async function ArticleDetail({ slug }: { slug: string }) { + const controller = blogContainer.get( + BLOG_SYMBOLS.IGetArticleBySlugController, + ); + const article = await controller({ slug }); + const queryClient = getQueryClient(); + queryClient.setQueryData( + ["blog", "articleBySlug", { input: { slug } }], + article, + ); + + return ( + + + + ); +} diff --git a/packages/blog/src/ui/components/article-list.tsx b/packages/blog/src/ui/components/article-list.client.tsx similarity index 100% rename from packages/blog/src/ui/components/article-list.tsx rename to packages/blog/src/ui/components/article-list.client.tsx diff --git a/packages/blog/src/ui/components/article-list.server.tsx b/packages/blog/src/ui/components/article-list.server.tsx new file mode 100644 index 0000000..4ca0196 --- /dev/null +++ b/packages/blog/src/ui/components/article-list.server.tsx @@ -0,0 +1,24 @@ +import { dehydrate, HydrationBoundary } from "@tanstack/react-query"; +import { getQueryClient } from "@repo/core-trpc"; +import { blogContainer } from "../../di/container"; +import { BLOG_SYMBOLS } from "../../di/symbols"; +import type { IGetArticlesController } from "../../interface-adapters/controllers/get-articles.controller"; +import { ArticleList as ArticleListClient } from "./article-list.client"; + +export async function ArticleList() { + const controller = blogContainer.get( + BLOG_SYMBOLS.IGetArticlesController, + ); + const articles = await controller({ status: "published", limit: 20 }); + const queryClient = getQueryClient(); + queryClient.setQueryData( + ["blog", "listArticles", { input: { status: "published", limit: 20 } }], + articles, + ); + + return ( + + + + ); +} diff --git a/packages/blog/src/ui/index.ts b/packages/blog/src/ui/index.ts index a20f21e..be51fa7 100644 --- a/packages/blog/src/ui/index.ts +++ b/packages/blog/src/ui/index.ts @@ -2,8 +2,5 @@ export { articleBySlugQuery, listArticlesQuery } from "./query"; export { useArticleList } from "./hooks/use-article-list"; export { useArticleBySlug } from "./hooks/use-article-by-slug"; export { ArticleCard, type ArticleCardProps } from "./components/article-card"; -export { ArticleList } from "./components/article-list"; -export { - ArticleDetail, - type ArticleDetailProps, -} from "./components/article-detail"; +export { ArticleList } from "./components/article-list.server"; +export { ArticleDetail } from "./components/article-detail.server"; diff --git a/packages/marketing-pages/src/ui/components/page-content.tsx b/packages/marketing-pages/src/ui/components/page-content.client.tsx similarity index 100% rename from packages/marketing-pages/src/ui/components/page-content.tsx rename to packages/marketing-pages/src/ui/components/page-content.client.tsx diff --git a/packages/marketing-pages/src/ui/components/page-content.server.tsx b/packages/marketing-pages/src/ui/components/page-content.server.tsx new file mode 100644 index 0000000..f2a353f --- /dev/null +++ b/packages/marketing-pages/src/ui/components/page-content.server.tsx @@ -0,0 +1,35 @@ +import { dehydrate, HydrationBoundary } from "@tanstack/react-query"; +import { getQueryClient } from "@repo/core-trpc"; +import { marketingPagesContainer } from "../../di/container"; +import { MARKETING_PAGES_SYMBOLS } from "../../di/symbols"; +import type { IGetPageBySlugController } from "../../interface-adapters/controllers/get-page-by-slug.controller"; +import { PageContent as PageContentClient } from "./page-content.client"; + +export async function PageContent({ slug }: { slug: string }) { + const controller = marketingPagesContainer.get( + MARKETING_PAGES_SYMBOLS.IGetPageBySlugController, + ); + const page = await controller({ slug }); + const queryClient = getQueryClient(); + queryClient.setQueryData( + ["marketingPages", "pageBySlug", { input: { slug } }], + page, + ); + + if (!page) { + return ( +
+

Not found

+

+ This page hasn't been published yet. +

+
+ ); + } + + return ( + + + + ); +} diff --git a/packages/marketing-pages/src/ui/index.ts b/packages/marketing-pages/src/ui/index.ts index 38dbca1..0314f52 100644 --- a/packages/marketing-pages/src/ui/index.ts +++ b/packages/marketing-pages/src/ui/index.ts @@ -2,4 +2,4 @@ export { pageBySlugQuery, siteSettingsQuery } from "./query"; export { usePageBySlug } from "./hooks/use-page-by-slug"; export { useSiteSettings } from "./hooks/use-site-settings"; export { PageHero, type PageHeroProps } from "./components/page-hero"; -export { PageContent, type PageContentProps } from "./components/page-content"; +export { PageContent } from "./components/page-content.server"; diff --git a/packages/navigation/src/ui/components/site-header.tsx b/packages/navigation/src/ui/components/site-header.client.tsx similarity index 100% rename from packages/navigation/src/ui/components/site-header.tsx rename to packages/navigation/src/ui/components/site-header.client.tsx diff --git a/packages/navigation/src/ui/components/site-header.server.tsx b/packages/navigation/src/ui/components/site-header.server.tsx new file mode 100644 index 0000000..8ab1a48 --- /dev/null +++ b/packages/navigation/src/ui/components/site-header.server.tsx @@ -0,0 +1,27 @@ +import { dehydrate, HydrationBoundary } from "@tanstack/react-query"; +import { getQueryClient } from "@repo/core-trpc"; +import { navigationContainer } from "../../di/container"; +import { NAVIGATION_SYMBOLS } from "../../di/symbols"; +import type { IGetHeaderController } from "../../interface-adapters/controllers/get-header.controller"; +import { SiteHeader as SiteHeaderClient } from "./site-header.client"; + +export async function SiteHeader({ + siteName, + siteDescription, +}: { + siteName: string; + siteDescription?: string; +}) { + const controller = navigationContainer.get( + NAVIGATION_SYMBOLS.IGetHeaderController, + ); + const header = await controller({}); + const queryClient = getQueryClient(); + queryClient.setQueryData(["navigation", "header", { input: {} }], header); + + return ( + + + + ); +} diff --git a/packages/navigation/src/ui/index.ts b/packages/navigation/src/ui/index.ts index 75e6e9a..920bac9 100644 --- a/packages/navigation/src/ui/index.ts +++ b/packages/navigation/src/ui/index.ts @@ -1,3 +1,3 @@ export { headerQuery } from "./query"; export { useHeader } from "./hooks/use-header"; -export { SiteHeader, type SiteHeaderProps } from "./components/site-header"; +export { SiteHeader } from "./components/site-header.server";