examples/02-composite-slots.tsx
examples/02-composite-slots.tsxBrowse 13 files
808 tokens
3,208 bytes
Token encoding: o200k_base
Snapshot 8e164d2
← Back to SKILL.md
1// Composite Component patterns:2// - children slot for free-form composition3// - render-prop slot when the server must pass data into client UI4// - component prop slot for reusable typed interactive controls5 6import type { ComponentType, ReactNode } from 'react'7import { createFileRoute } from '@tanstack/react-router'8import { createServerFn } from '@tanstack/react-start'9import {10 CompositeComponent,11 createCompositeComponent,12} from '@tanstack/react-start/rsc'13import { z } from 'zod'14 15// Replace with your own server-only data layer16declare const db: {17 posts: {18 findById(postId: string): Promise<{19 id: string20 authorId: string21 title: string22 body: string23 }>24 }25 products: {26 findById(productId: string): Promise<{27 id: string28 name: string29 price: number30 }>31 }32}33 34const getPostCard = createServerFn({ method: 'GET' })35 .validator(z.object({ postId: z.string() }))36 .handler(async ({ data }) => {37 const post = await db.posts.findById(data.postId)38 39 const src = await createCompositeComponent<{40 children?: ReactNode41 renderActions?: (args: { postId: string; authorId: string }) => ReactNode42 }>((props) => (43 <article className="card">44 <h1>{post.title}</h1>45 <p>{post.body}</p>46 47 <footer>48 {props.renderActions?.({49 postId: post.id,50 authorId: post.authorId,51 })}52 </footer>53 54 <section>{props.children}</section>55 </article>56 ))57 58 return { src }59 })60 61export const Route = createFileRoute('/posts/$postId')({62 loader: async ({ params }) =>63 getPostCard({ data: { postId: params.postId } }),64 component: PostPage,65})66 67function PostPage() {68 const { src } = Route.useLoaderData()69 70 return (71 <CompositeComponent72 src={src}73 renderActions={({ postId, authorId }) => (74 <PostActions postId={postId} authorId={authorId} />75 )}76 >77 <Comments />78 </CompositeComponent>79 )80}81 82function Comments() {83 return <div>Interactive comments go here</div>84}85 86function PostActions(props: { postId: string; authorId: string }) {87 return (88 <button type="button">89 Moderate {props.postId} / {props.authorId}90 </button>91 )92}93 94// Component-prop slot variant95const getProductCard = createServerFn({ method: 'GET' })96 .validator(z.object({ productId: z.string() }))97 .handler(async ({ data }) => {98 const product = await db.products.findById(data.productId)99 100 const src = await createCompositeComponent<{101 AddToCart?: ComponentType<{ productId: string; price: number }>102 }>((props) => (103 <section className="product-card">104 <h2>{product.name}</h2>105 {props.AddToCart ? (106 <props.AddToCart productId={product.id} price={product.price} />107 ) : null}108 </section>109 ))110 111 return { src }112 })113 114function AddToCartButton(props: { productId: string; price: number }) {115 return (116 <button type="button">117 Add {props.productId} at {props.price}118 </button>119 )120}121 122// Example usage somewhere else:123// const { src } = await getProductCard({ data: { productId: 'p-1' } })124// <CompositeComponent src={src} AddToCart={AddToCartButton} />125