Skip to content

Commit 88ef80d

Browse files
committed
checkpoint
1 parent 82bf777 commit 88ef80d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1171
-325
lines changed

AGENTS.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,65 @@ For runtime testing and verification, developers should:
184184
1. Review the code changes
185185
2. Start the dev server manually (`pnpm dev`)
186186
3. Test the functionality in a browser
187+
188+
## UI Style Guide 2026
189+
190+
### Core Principles
191+
192+
- Prioritize clarity, hierarchy, and calm
193+
- Use depth to communicate structure, not decoration
194+
- Favor warmth and approachability over stark minimalism
195+
196+
### Layout
197+
198+
- Prefer fewer, well defined containers over many small sections
199+
- Use generous spacing to create separation before adding visual effects
200+
- Cards are acceptable when they express grouping or hierarchy
201+
202+
### Corners
203+
204+
- Rounded corners are standard
205+
- Use subtle radius values that feel intentional, not playful
206+
- Avoid sharp 90 degree corners unless intentionally industrial
207+
208+
### Shadows and Depth
209+
210+
- Shadows should be soft, low contrast, and diffused
211+
- Use shadows to imply separation, not elevation theatrics
212+
- Avoid heavy drop shadows or strong directional lighting
213+
- One to two shadow layers max
214+
215+
### Cards
216+
217+
- Cards should feel grounded, not floating
218+
- Prefer light elevation, border plus shadow, or surface contrast
219+
- Avoid overusing cards as a default layout primitive
220+
221+
### Color and Surfaces
222+
223+
- Favor soft neutrals, off whites, and warm grays
224+
- Use surface contrast or translucency instead of strong outlines
225+
- Glass or frosted effects are acceptable when subtle and accessible
226+
227+
### Interaction
228+
229+
- Use micro transitions to reinforce spatial relationships
230+
- Hover and focus states should feel responsive, not animated
231+
- Avoid excessive motion or springy effects
232+
233+
### Typography
234+
235+
- Let type hierarchy do most of the work
236+
- Strong headings, calm body text
237+
- Avoid visual noise around content
238+
239+
### What to Avoid
240+
241+
- Chunky shadows
242+
- Overly flat, sterile layouts
243+
- Neumorphism as a primary style
244+
- Over designed card grids
245+
246+
### Summary Rule
247+
248+
**If depth does not improve comprehension, remove it.**

opencode.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"$schema": "https://opencode.ai/config.json",
3+
"mcp": {
4+
"playwright": {
5+
"type": "local",
6+
"command": ["npx", "-y", "@playwright/mcp@latest"],
7+
"enabled": true
8+
}
9+
}
10+
}

src/blog/tanstack-start-rsc.md

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
---
2+
title: What If RSCs Were Actually Components?
3+
published: 2025-01-15
4+
authors:
5+
- Tanner Linsley
6+
- Manuel Schiller
7+
---
8+
9+
React Server Components are a genuine leap for React. They reduce bundle size, stream UI as it resolves, and move work off the client.
10+
11+
But the current model comes with a tradeoff: **the server owns the component tree**.
12+
Your client code opts into interactivity with `'use client'`. **Composition flows one direction**—server decides, client receives. The React model you know—props, context, bidirectional composition—gets fragmented across environments.
13+
14+
What if it didn't have to?
15+
16+
What if RSCs were actually components? **Fetchable. Cacheable. Composable in both directions.** Primitives that flow through your router, your cache, your data layer—without special directives or framework lock-in.
17+
18+
That's what we built in **TanStack Start**.
19+
20+
---
21+
22+
## The One-Way Problem
23+
24+
In the traditional RSC model, the server is the only place that decides what interactive elements to render.
25+
26+
Need a `<Link>` with prefetching? **Server** renders it with `'use client'`.
27+
Need a dashboard widget? Server renders the boundary, marks it client. **The server is always the decision-maker. The client is always the recipient.**
28+
29+
This works. **But it's also limiting.**
30+
31+
What if the server wants to render some interactive elements directly (like `<Link>`) but also **defer entire regions of interactivity back to the client**?
32+
In the traditional model, you'd create a new client component, a new file, a new boundary. **Every deferral is a seam.**
33+
34+
---
35+
36+
## Bidirectional Composition
37+
38+
**TanStack Start flips the constraint.**
39+
40+
**Server components can render interactive elements directly**`<Link>`, `<Button>`, anything marked `'use client'`. That part works the same.
41+
42+
But they can also **declare slots**—regions where the client decides what to render. Not by creating new components or files, but through plain props: children, render functions, whatever pattern you already use.
43+
44+
```tsx
45+
// Server
46+
const getPost = createServerFn().handler(async ({ data }) => {
47+
const post = await db.posts.get(data.postId)
48+
49+
return createServerComponent(
50+
(props: {
51+
children?: React.ReactNode
52+
renderActions?: (data: {
53+
postId: string
54+
authorId: string
55+
}) => React.ReactNode
56+
}) => (
57+
<article>
58+
<h1>{post.title}</h1>
59+
<p>{post.body}</p>
60+
61+
{/* Server renders this directly—it's interactive via 'use client' */}
62+
<Link to={`/posts/${post.nextPostId}`}>Next Post</Link>
63+
64+
{/* Server defers this to the client */}
65+
<footer>
66+
{props.renderActions?.({ postId: post.id, authorId: post.authorId })}
67+
</footer>
68+
69+
{/* Client decides what goes here */}
70+
{props.children}
71+
</article>
72+
),
73+
)
74+
})
75+
```
76+
77+
```tsx
78+
// Client
79+
function PostPage({ postId }) {
80+
const { data: Post } = useQuery({
81+
queryKey: ['post', postId],
82+
queryFn: () => getPost({ data: { postId } }),
83+
})
84+
85+
if (!Post) return <PostSkeleton />
86+
87+
return (
88+
<Post
89+
renderActions={({ postId, authorId }) => (
90+
// Full client interactivity—hooks, state, context, all of it
91+
<PostActions postId={postId} authorId={authorId} />
92+
)}
93+
>
94+
<Comments postId={postId} />
95+
</Post>
96+
)
97+
}
98+
```
99+
100+
**The server rendered the `<Link>` directly.** It also passed `postId` and `authorId` through the slot so the client could render `<PostActions>` with that data. And it left children open for the client to fill with `<Comments>`.
101+
102+
**Both directions. Same component. No new files. No new boundaries.**
103+
That's inversion of control.
104+
105+
---
106+
107+
## RSCs as a Primitive
108+
109+
Here's the shift: in TanStack Start, **server components aren't a paradigm**. They're a _primitive_—a serialization format that flows through your existing architecture.
110+
111+
When `createServerFn` returns a server component, that component is a **stream**. Streams are universal. They work with:
112+
113+
- **TanStack Router** → Load server components in route loaders, stream them during navigation
114+
- **TanStack Query** → Cache server components, refetch in the background, deduplicate requests
115+
- **TanStack DB** (coming soon) → Sync server component state, offline support, optimistic updates
116+
117+
```tsx
118+
// In a route loader
119+
export const Route = createFileRoute('/posts/$postId')({
120+
loader: async ({ params }) => ({
121+
Post: await getPost({ data: { postId: params.postId } }),
122+
}),
123+
component: PostPage,
124+
})
125+
126+
function PostPage() {
127+
const { Post } = Route.useLoaderData()
128+
129+
return (
130+
<Post renderActions={({ postId }) => <PostActions postId={postId} />}>
131+
<Comments />
132+
</Post>
133+
)
134+
}
135+
```
136+
137+
```tsx
138+
// With Query caching
139+
const { data: Layout } = useQuery({
140+
queryKey: ['layout'],
141+
queryFn: () => getLayout(),
142+
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
143+
})
144+
```
145+
146+
**The server component is just data.** Fetch it, cache it, transform it, compose it. **No special APIs. No framework-specific caching layers.** Just streams flowing through tools you already know.
147+
148+
---
149+
150+
## How It Works
151+
152+
When your server component accesses props, it's accessing a **proxy**.
153+
Every property access and function call is tracked:
154+
155+
- `props.children` → serialized as a slot placeholder
156+
- `props.renderActions({ postId, authorId })` → serialized with the arguments attached
157+
158+
**Over the wire, it's a React element stream** with embedded placeholders. On the client:
159+
160+
1. The stream decodes into a React element tree
161+
2. Placeholders match the props you passed when rendering
162+
3. Render functions replay with the serialized arguments
163+
164+
```
165+
Server Client
166+
─────── ──────
167+
props.renderActions({ renderActions prop is called
168+
postId: "abc", → with { postId: "abc", authorId: "xyz" }
169+
authorId: "xyz"
170+
}) Your function runs client-side
171+
with full hooks/state/context
172+
```
173+
174+
**Type safety flows through automatically.** The function signature on the server determines what arguments your client function receives:
175+
176+
```tsx
177+
const getPost = createServerFn().handler(async ({ data }) => {
178+
const post = await db.posts.get(data.postId)
179+
180+
return createServerComponent(
181+
(props: {
182+
renderActions?: (data: {
183+
postId: string
184+
authorId: string
185+
}) => React.ReactNode
186+
}) => {
187+
// TypeScript knows exactly what props.renderActions receives
188+
props.renderActions?.({ postId: post.id, authorId: post.authorId })
189+
// ...
190+
},
191+
)
192+
})
193+
```
194+
195+
---
196+
197+
## The Full Spectrum
198+
199+
With RSCs as primitives, TanStack Start covers every frontend use case:
200+
201+
- **Fully Interactive**
202+
No server components at all. Client-first, SPA-style. RSCs are an optimization you add when helpful, not a paradigm you build around.
203+
204+
- **Hybrid**
205+
Server components for static shells, data-heavy regions, or SEO-critical content. Slots for interactivity. Mix freely within the same component.
206+
207+
- **Fully Static**
208+
Pre-render everything at build time. No hydration, no JavaScript. Ship HTML.
209+
210+
**One framework. One mental model. The entire spectrum.**
211+
You don't choose "interactive framework" or "static framework" or "RSC framework."
212+
You choose patterns **per-route, per-component, per-use-case**. The architecture supports all of it.
213+
214+
---
215+
216+
## The Philosophy
217+
218+
React Server Components are powerful. They unlock patterns that weren't possible before.
219+
220+
But the traditional model makes a choice: **the server owns the tree, and interactivity is an escape hatch.** That works for content-heavy sites. It creates friction for apps that are fundamentally interactive.
221+
222+
**TanStack Start doesn't make that choice for you.**
223+
224+
> RSCs are a serialization format—a way to stream React elements from the server. They're a primitive, not a paradigm. Use them when they help. Compose around them when you need control. The client and server are peers, not a hierarchy.
225+
226+
The server can render interactive elements directly. It can also defer to the client through slots. **Composition flows both directions.**
227+
You decide the balance, per-component, based on what makes sense.
228+
229+
Because it's not about client or server.
230+
**It's about using both—intentionally.**
231+
232+
---
233+
234+
## Current Status: Experimental
235+
236+
This is the first experimental release of RSCs in TanStack Start. A few things to know:
237+
238+
**React's Flight serializer** — This release uses React's native RSC Flight protocol for serialization. That means TanStack Start's usual Seroval-based serialization isn't available within server components for now. Standard JavaScript primitives, Dates, and React elements serialize fine. Custom Seroval plugins and extended types will come in a future release as we unify the serialization layers.
239+
240+
**API surface** — The `createServerComponent` API and slot patterns shown here are stable in design but may see refinements based on feedback. We're shipping early to learn from real usage.
241+
242+
**Performance** — Streaming works today. We're continuing to optimize the ReplayableStream buffering, frame protocol efficiency, and cache integration.
243+
244+
If you hit rough edges, [open an issue](https://github.com/tanstack/router/issues) or drop into [Discord](https://tlinz.com/discord). This is the time to shape the API.
245+
246+
---
247+
248+
## FAQ
249+
250+
### How does this compare to Next.js App Router?
251+
252+
Next.js App Router is server-first: your component tree lives on the server by default, and you opt into client interactivity with `'use client'` boundaries.
253+
254+
TanStack Start is **isomorphic-first**: your tree lives wherever makes sense, and RSCs are a primitive you pull in when helpful. The key difference is **bidirectional composition**—server components can defer to the client through slots, not just the other way around.
255+
256+
Both approaches support RSCs. They differ in who owns the tree by default and how composition flows.
257+
258+
### Can I use this with my existing Next.js/Remix app?
259+
260+
Not directly—TanStack Start is its own framework built on TanStack Router. But if you're using TanStack Query or TanStack Router already, the mental model transfers. Server components become another data source that Query can cache and Router can load.
261+
262+
### Do I have to use RSCs?
263+
264+
No. RSCs are entirely opt-in. You can build fully client-side SPAs with TanStack Start, use traditional SSR without server components, or go fully static. RSCs are one tool in the spectrum, not a requirement.
265+
266+
### What about React 19 / `use` / Server Actions?
267+
268+
TanStack Start's RSC implementation builds on React's Flight protocol and works with React 19. Server Actions are a separate primitive—`createServerFn` serves a similar purpose but integrates with TanStack's middleware, validation, and caching model. We're watching the Server Actions API and will align where it makes sense.
269+
270+
### When will Seroval serialization work inside RSCs?
271+
272+
It's on the roadmap. The current release uses React's Flight serializer directly, which handles the core use cases. Unifying with Seroval for custom types, extended serialization, and tighter TanStack DB integration is planned for a future release.
273+
274+
---
275+
276+
## Get Started
277+
278+
TanStack Start's RSC model is available now in experimental.
279+
280+
- [Documentation](https://tanstack.com/start)
281+
- [GitHub](https://github.com/tanstack/router)
282+
- [Discord](https://tlinz.com/discord)

src/components/BackgroundGradient.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@ export function BackgroundGradient() {
1111
const matches = useMatches()
1212

1313
if (!libraryId) {
14-
const matchIndex = matches.findIndex((m) => m.id === '/_libraries')
14+
const matchIndex = matches.findIndex((m) => m.routeId === '/_libraries')
1515
const match = matches[matchIndex + 1]
16-
libraryId = match.routeId.split('/')[2]
16+
if (match) {
17+
// Route ID format: /_libraries/query/$version/ -> split gives ['', '_libraries', 'query', '$version', '']
18+
libraryId = match.routeId.split('/')[2]
19+
}
1720
}
1821

1922
const library = findLibrary(libraryId as any)

src/components/BrandContextMenu.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,8 @@ export function BrandContextMenu({ children, ...rest }: BrandContextMenuProps) {
110110
className={twMerge(
111111
'p-1 rounded-full',
112112
darkBg
113-
? 'bg-black text-white shadow-lg shadow-white/20'
114-
: 'bg-white text-black shadow-lg',
113+
? 'bg-black text-white shadow-md'
114+
: 'bg-white text-black shadow-md',
115115
)}
116116
>
117117
<img src={url} alt={label} className="h-6" />

src/components/ChatPanel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export function ChatPanel({ messages, typingUserMessage }: ChatPanelProps) {
1212

1313
return (
1414
<div
15-
className={`flex flex-col w-full md:w-[400px] h-full rounded-xl overflow-hidden shadow-2xl ${
15+
className={`flex flex-col w-full md:w-[400px] h-full rounded-xl overflow-hidden shadow-lg ${
1616
isDark
1717
? 'bg-gradient-to-b from-gray-900/95 to-gray-950/95 border border-gray-800/50 backdrop-blur-sm'
1818
: 'bg-gradient-to-b from-white/95 to-gray-50/95 border border-gray-200/50 backdrop-blur-sm'

0 commit comments

Comments
 (0)