Composing
Server components inside client scripts, client components inside server components, and the phone's state handed to your server's layout.
server/ProductPage.tsx
import { cs } from "@backtickjs/core";import { useState } from "@backtickjs/react";import { Pressable, ScrollView, Text, View } from "@backtickjs/react-native";import type { JSX } from "@backtickjs/react/jsx-runtime";import { reviewsFor } from "./reviews.js";// A client component.const LikeButton = cs`() => {const [liked, setLiked] = $useState(false);return (<$Pressable onPress={() => setLiked(!liked)}><$Text>{liked ? "♥ Liked" : "♡ Like"}</$Text></$Pressable>);}`;// A server component, drawn inside other scripts.async function Reviews({ productId }: { productId: string }) {const reviews = await reviewsFor(productId);return cs`(<$View style={{ gap: 8 }}>{$reviews.map((review) => (<$Text key={review.id}>{review.author}: {review.text}</$Text>))}</$View>)`;}// A server component that lays out what it's given.function Section({title,children,}: {title: string;children: JSX.Element;}) {return cs`(<$View style={{ gap: 12 }}><$Text style={{ fontSize: 20, fontWeight: "bold" }}>{$title}</$Text>{$children}</$View>)`;}export async function ProductPage({ productId }: { productId: string }) {return cs`(<$ScrollView contentContainerStyle={{ padding: 24, gap: 24 }}><$Text style={{ fontSize: 28, fontWeight: "bold" }}>Ceramic dripper</$Text><$LikeButton />{${(<Section title="Reviews"><Reviews productId={productId} /></Section>)}}</$ScrollView>)`;}
Server and client components compose in both directions, in one file:
- A client component inside a script:
<$LikeButton />. - A server component inside a script:
{${<Reviews productId={…} />}}, a splice of its element. It runs on your server when the bundle is built, and the phone gets only what it returns. - A server component inside a server component:
Sectiontakes<Reviews />as its children and draws them with{$children}.
Each place it's drawn, it runs
A server component's element runs its component once per place it's drawn, as React renders an element at each place it stands. Drawn twice, it runs twice. If two places need the same data, fetch it once and pass it down, or cache the fetch for the request.
Handing it the phone's state
A server component drawn inside a client component can be handed that component's local values, as client code:
server/LiveSection.tsx
import { type Client, cs } from "@backtickjs/core";import { useState } from "@backtickjs/react";import { Pressable, Text, View } from "@backtickjs/react-native";const Like = cs`() => {const [liked, setLiked] = $useState(false);return (<$Pressable onPress={() => setLiked(!liked)}><$Text>{liked ? "♥" : "♡"}</$Text></$Pressable>);}`;// A server component handed client code: its title is the phone's to compute.async function Section({ title }: { title: Client<string> }) {return cs`(<$View><$Text>{$title}</$Text><$Like /></$View>)`;}// A client component drawing that server component, handing it a local.const Cart = cs`() => {const [count, setCount] = $useState(0);return (<$View><$Pressable onPress={() => setCount(count + 1)}><$Text>Add</$Text></$Pressable>{${(<Section title={cs`count + " in your cart"`} />)}}</$View>);}`;export async function Home() {return cs`<$Cart />`;}
cs`count + " in your cart"`is a fragment ofCart's code, reading itscount.Sectionsplices it as$title, and the phone computes it each time it's read, so the title follows the count.Likekeeps its state while the title changes: it's a client component, one function for the bundle.Sectionruns once, on your server. It decides the layout; the phone fills in what depends on its state.