# Quick start Create a React Native app with a Backtick server in one command, then write a screen, serve it per request and draw it in your app. ```sh npx create-backtick-app@latest ``` It asks for a name, then creates an Expo app with a Backtick server beside it, on Node, and installs its packages. Give the name on the command line, `npx create-backtick-app@latest my-app`, and it asks nothing. It needs Node 22 (22.15 or later) or 24 (24.3 or later), not 23. For a web page instead, pass `--template react` or `--template solid-js`; for a server on Bun, `--runtime bun`. ## Open it on your phone 1. Install Expo Go on your phone, from the App Store or Google Play. 2. Connect your phone to the same Wi-Fi network as your computer. 3. Start your project: ```sh cd my-app npm start ``` It starts your Backtick server and Expo together, and shows a QR code. 4. Scan the QR code: with the Camera app on iPhone, or with Expo Go on Android. No phone at hand? `npm run ios` opens the app in the iOS Simulator, `npm run android` in an Android emulator, and `npm run web` in a browser. The app shows a welcome screen. It isn't in the app: your server sent it. ## Change the screen The screen is `server/Home.tsx`. Replace it with this, and save: ```tsx title="server/Home.tsx" import { cs } from "@backtickjs/core"; import { useState } from "@backtickjs/react"; import { Pressable, Text, View } from "@backtickjs/react-native"; // A client component: it runs on the phone. const Counter = cs`() => { const [count, setCount] = $useState(0); return ( <$Pressable onPress={() => setCount(count + 1)}> <$Text style={{ fontSize: 18, color: "#0a7ea4" }}> Tapped {count} times ); }`; // A server component: it runs on your server, for every request. export async function Home() { const time = new Date().toLocaleTimeString(); return cs`( <$View style={{ flex: 1, justifyContent: "center", alignItems: "center", gap: 16, }} > <$Text style={{ fontSize: 24 }}>Served at {$time} <$Counter /> )`; } ``` The app redraws with the new screen, without rebuilding. Tap the counter, then save again: the time changes, because the server ran `Home` again. What you just wrote: - **`Home` is a server component.** It runs on your server, for every request. Read from a database here, call an API, use any npm package: none of it reaches the phone. - **`` cs`…` `` is a client script.** The code inside the backticks runs on the phone. Here, it's the screen's JSX. - **`$time` is a splice.** It's a value from your server, written into the screen as data. TypeScript checks it on both sides. - **`Counter` is a client component.** It's a script that takes props, used as `<$Counter />`. Its state lives on the phone. - **`$View` and `$useState` are React Native and React,** with the same names and types, spliced from `@backtickjs/react-native` and `@backtickjs/react`. ## What's in your project ```text my-app/ ├── App.tsx The app: fetches each screen from your server and draws it ├── server/ │ ├── index.tsx Your server: bundles a screen per request │ ├── Home.tsx The screen you just changed │ ├── Home.test.tsx A test that draws the welcome screen, run by npm test │ ├── HelloWave.tsx The welcome screen's waving hand, safe to delete │ └── test/ What the test draws the screen with ├── scripts/ What npm start runs: your server and Expo together └── package.json ``` Everything in `server/` reaches users without an app release: deploy your server, and users get the change the next time they open the screen. `npm run typecheck` checks the app and your server, the code inside every `` cs`…` `` included. `npm test` draws the welcome screen and checks what it shows, so it fails now that you've changed the screen; the tutorial writes a test for yours. `npm run reset-project` replaces `server/Home.tsx` with a blank screen, rewrites the test to match, and deletes `HelloWave.tsx`, when you're ready to start your own. ## Next steps - **[Tutorial](/docs/tutorial):** build a coffee-ordering screen, step by step, in the project you just created. - **[Thinking in Backtick](/docs/thinking-in-backtick):** what runs where, and what crosses between your server and the phone. --- # Tutorial Build a coffee-ordering screen in six steps: a menu from your server, an order kept on the phone, sent back, and remembered for next time. Each step shows `server/Home.tsx`, with what changed marked. Start from the project the [Quick start](/docs) creates, with the app running, and clear the welcome screen: ```sh npm run reset-project ``` It also rewrites `server/Home.test.tsx` to test the blank screen. Delete that file: the last step writes one for the screen you're about to build. ## 1. Show the menu The menu is data your server reads. Here it's a file; yours could come from a database or an API. Create `server/menu.ts`: ```ts title="server/menu.ts" // The menu. Yours could come from a database or an API: anything your server // can read. export type Coffee = { id: string; name: string; price: number }; export async function getMenu(): Promise { return [ { id: "flat-white", name: "Flat white", price: 4.5 }, { id: "cortado", name: "Cortado", price: 4 }, { id: "cold-brew", name: "Cold brew", price: 5 }, ]; } ``` Then replace `server/Home.tsx`: ```tsx title="server/Home.tsx" import { cs } from "@backtickjs/core"; import { ScrollView, StatusBar, StyleSheet, Text, View, } from "@backtickjs/react-native"; import { getMenu } from "./menu.js"; export async function Home() { const menu = await getMenu(); return cs`{ const styles = $StyleSheet.create({ screen: { padding: 24, paddingTop: ($StatusBar.currentHeight ?? 0) + 24, gap: 16, }, title: { fontSize: 32, fontWeight: "bold" }, row: { flexDirection: "row", justifyContent: "space-between" }, name: { fontSize: 18 }, price: { fontSize: 18, color: "#666" }, }); return ( <$ScrollView contentInsetAdjustmentBehavior="automatic" contentContainerStyle={styles.screen} > <$Text style={styles.title}>Menu {$menu.map((coffee) => ( <$View key={coffee.id} style={styles.row}> <$Text style={styles.name}>{coffee.name} <$Text style={styles.price}> {coffee.price.toLocaleString("en-US", { style: "currency", currency: "USD", })} ))} ); }`; } ``` Save, and the menu appears. `Home` awaited it on your server, and `$menu` wrote it into the screen, typed as `getMenu` returns it. The styles are made in the script, with `$StyleSheet.create`: they're code that runs on the phone, like the JSX. ## 2. Add a client component Each coffee gets an "Add" button that counts its taps. A tap is handled on the phone, so the button is a client component: a script that takes props, with state of its own. ```diff title="server/Home.tsx" @@ -1,5 +1,7 @@ import { cs } from "@backtickjs/core"; +import { useState } from "@backtickjs/react"; import { + Pressable, ScrollView, StatusBar, StyleSheet, @@ -8,6 +10,18 @@ } from "@backtickjs/react-native"; import { getMenu } from "./menu.js"; +// A client component: it runs on the phone, with state of its own. +const AddButton = cs`() => { + const [count, setCount] = $useState(0); + return ( + <$Pressable onPress={() => setCount(count + 1)}> + <$Text style={{ fontSize: 18, color: "#0a7ea4" }}> + {count === 0 ? "Add" : "Added " + count} + + + ); +}`; + export async function Home() { const menu = await getMenu(); @@ -39,6 +53,7 @@ currency: "USD", })} + <$AddButton /> ))} ``` Tap "Add" a few times. Each button keeps its own count: `AddButton` is a client component, like `Counter` in the quick start, and each tag is its own instance. ## 3. Share the order across the screen Counts kept in each button can't add up to a total. The order belongs one level up, in a client component that holds every count and draws the rows. ```diff title="server/Home.tsx" @@ -8,55 +8,85 @@ Text, View, } from "@backtickjs/react-native"; -import { getMenu } from "./menu.js"; +import { type Coffee, getMenu } from "./menu.js"; -// A client component: it runs on the phone, with state of its own. -const AddButton = cs`() => { - const [count, setCount] = $useState(0); - return ( - <$Pressable onPress={() => setCount(count + 1)}> - <$Text style={{ fontSize: 18, color: "#0a7ea4" }}> - {count === 0 ? "Add" : "Added " + count} +// The screen's styles: a script too, one the components below share. +const styles = cs`$StyleSheet.create({ + screen: { + padding: 24, + paddingTop: ($StatusBar.currentHeight ?? 0) + 24, + gap: 16, + }, + title: { fontSize: 32, fontWeight: "bold" }, + row: { flexDirection: "row", justifyContent: "space-between" }, + name: { fontSize: 18 }, + add: { fontSize: 18, color: "#0a7ea4" }, + total: { fontSize: 18, fontWeight: "600", marginTop: 8 }, +})`; + +// A client function, which any script can call. +const formatPrice = cs`(price: number) => + price.toLocaleString("en-US", { style: "currency", currency: "USD" })`; + +// One coffee, its price until it's in the order, then how many are. +const CoffeeRow = cs`(props: { + coffee: Coffee; + count: number; + onAdd: () => void; +}) => ( + <$View style={$styles.row}> + <$Text style={$styles.name}>{props.coffee.name} + <$Pressable onPress={props.onAdd}> + <$Text style={$styles.add}> + {props.count === 0 + ? $formatPrice(props.coffee.price) + : props.count + " ×"} + +)`; + +// The order: how many of each coffee, kept on the phone. +const Order = cs`(props: { menu: Coffee[] }) => { + const [counts, setCounts] = $useState>({}); + const add = (id: string) => + setCounts({ ...counts, [id]: (counts[id] ?? 0) + 1 }); + + const items = Object.values(counts).reduce((sum, n) => sum + n, 0); + const total = props.menu.reduce( + (sum, coffee) => sum + (counts[coffee.id] ?? 0) * coffee.price, + 0, ); + + return ( + <$View> + {props.menu.map((coffee) => ( + <$CoffeeRow + key={coffee.id} + coffee={coffee} + count={counts[coffee.id] ?? 0} + onAdd={() => add(coffee.id)} + /> + ))} + <$Text style={$styles.total}> + {items === 0 + ? "Tap a price to add it." + : items + " in your order · " + $formatPrice(total)} + + + ); }`; export async function Home() { const menu = await getMenu(); - return cs`{ - const styles = $StyleSheet.create({ - screen: { - padding: 24, - paddingTop: ($StatusBar.currentHeight ?? 0) + 24, - gap: 16, - }, - title: { fontSize: 32, fontWeight: "bold" }, - row: { flexDirection: "row", justifyContent: "space-between" }, - name: { fontSize: 18 }, - price: { fontSize: 18, color: "#666" }, - }); - - return ( - <$ScrollView - contentInsetAdjustmentBehavior="automatic" - contentContainerStyle={styles.screen} - > - <$Text style={styles.title}>Menu - {$menu.map((coffee) => ( - <$View key={coffee.id} style={styles.row}> - <$Text style={styles.name}>{coffee.name} - <$Text style={styles.price}> - {coffee.price.toLocaleString("en-US", { - style: "currency", - currency: "USD", - })} - - <$AddButton /> - - ))} - - ); - }`; + return cs`( + <$ScrollView + contentInsetAdjustmentBehavior="automatic" + contentContainerStyle={$styles.screen} + > + <$Text style={$styles.title}>Menu + <$Order menu={$menu} /> + + )`; } ``` Tap a price to add a coffee; the total updates below. - **`styles` and `formatPrice` are scripts too,** a value and a function rather than components. Each component splices the ones it uses, and the bundle carries each script once. - **`Order` holds the state; `CoffeeRow` gets it as props,** a count and an `onAdd` callback, as in any React app. Inside client code, props can be anything: functions included. ## 4. Send the order to your server The order is placed with a `fetch` from the phone. First, the server needs somewhere to keep orders. Create `server/orders.ts`: ```ts title="server/orders.ts" // Orders, kept in memory while the server runs. Yours would go to a database. export type Counts = Record; let last: Counts | null = null; export async function saveOrder(counts: Counts): Promise { last = counts; } export async function lastOrder(): Promise { return last; } ``` Then add a route for them to `server/index.tsx`, and pass `Home` the address the app reached your server at: ```diff title="server/index.tsx" @@ -1,7 +1,9 @@ import { createServer } from "node:http"; import { createRequire } from "node:module"; +import { text } from "node:stream/consumers"; import { bundler } from "@backtickjs/bundler"; import { Home } from "./Home.js"; +import { saveOrder } from "./orders.js"; // Development is only what `npm start` runs, which sets NODE_ENV: any other // run, a deploy included, is production. In development, this run of the @@ -27,9 +29,19 @@ response.end(run); return; } + if (request.method === "POST" && request.url === "/orders") { + await saveOrder(JSON.parse(await text(request))); + response.end(); + return; + } if (request.url === "/home") { + // Where the app reached this server, for the screen to send its order to. + const origin = `http://${request.headers.host}`; try { - const bundle = await bundler.build({ input: , packageVersions }); + const bundle = await bundler.build({ + input: , + packageVersions, + }); const { code } = bundle.generate({ format: "cjs" }); response.setHeader("content-type", "text/javascript"); response.end(code); ``` Now the screen can send its order there: ```diff title="server/Home.tsx" @@ -22,6 +22,14 @@ name: { fontSize: 18 }, add: { fontSize: 18, color: "#0a7ea4" }, total: { fontSize: 18, fontWeight: "600", marginTop: 8 }, + button: { + marginTop: 16, + padding: 14, + borderRadius: 12, + alignItems: "center", + backgroundColor: "#0a7ea4", + }, + buttonText: { fontSize: 18, fontWeight: "600", color: "#fff" }, })`; // A client function, which any script can call. @@ -46,11 +54,19 @@ )`; -// The order: how many of each coffee, kept on the phone. -const Order = cs`(props: { menu: Coffee[] }) => { +// The order: how many of each coffee, kept on the phone until it's placed. +const Order = cs`(props: { menu: Coffee[]; ordersUrl: string }) => { const [counts, setCounts] = $useState>({}); + const [placed, setPlaced] = $useState(false); const add = (id: string) => setCounts({ ...counts, [id]: (counts[id] ?? 0) + 1 }); + const place = async () => { + await fetch(props.ordersUrl, { + method: "POST", + body: JSON.stringify(counts), + }); + setPlaced(true); + }; const items = Object.values(counts).reduce((sum, n) => sum + n, 0); const total = props.menu.reduce( @@ -73,12 +89,20 @@ ? "Tap a price to add it." : items + " in your order · " + $formatPrice(total)} + {items > 0 && ( + <$Pressable style={$styles.button} onPress={place} disabled={placed}> + <$Text style={$styles.buttonText}> + {placed ? "Ordered ✓" : "Place order"} + + + )} ); }`; -export async function Home() { +export async function Home({ origin }: { origin: string }) { const menu = await getMenu(); + const ordersUrl = origin + "/orders"; return cs`( <$ScrollView @@ -86,7 +110,7 @@ contentContainerStyle={$styles.screen} > <$Text style={$styles.title}>Menu - <$Order menu={$menu} /> + <$Order menu={$menu} ordersUrl={$ordersUrl} /> )`; } ``` Add a coffee, then tap "Place order". - **`Home` takes props now.** The server reads them from the request, here the `origin`, and `Home` turns them into the screen. - **The `/orders` route is an ordinary handler:** the screen talks to your server like any app would, with a `fetch` from the phone. ## 5. Make it personal The server knows your last order, so the screen can start with it. ```diff title="server/Home.tsx" @@ -9,6 +9,7 @@ View, } from "@backtickjs/react-native"; import { type Coffee, getMenu } from "./menu.js"; +import { type Counts, lastOrder } from "./orders.js"; // The screen's styles: a script too, one the components below share. const styles = cs`$StyleSheet.create({ @@ -18,6 +19,7 @@ gap: 16, }, title: { fontSize: 32, fontWeight: "bold" }, + usual: { fontSize: 16, color: "#666" }, row: { flexDirection: "row", justifyContent: "space-between" }, name: { fontSize: 18 }, add: { fontSize: 18, color: "#0a7ea4" }, @@ -55,8 +57,13 @@ )`; // The order: how many of each coffee, kept on the phone until it's placed. -const Order = cs`(props: { menu: Coffee[]; ordersUrl: string }) => { - const [counts, setCounts] = $useState>({}); +// It starts as the order the server gives it. +const Order = cs`(props: { + menu: Coffee[]; + ordersUrl: string; + initialCounts: Counts; +}) => { + const [counts, setCounts] = $useState(props.initialCounts); const [placed, setPlaced] = $useState(false); const add = (id: string) => setCounts({ ...counts, [id]: (counts[id] ?? 0) + 1 }); @@ -100,9 +107,20 @@ ); }`; +// A server component, drawn inside the script below: your last order, in +// words. +async function Usual({ counts, menu }: { counts: Counts; menu: Coffee[] }) { + const summary = menu + .filter((coffee) => counts[coffee.id]) + .map((coffee) => `${counts[coffee.id]} × ${coffee.name}`) + .join(", "); + return cs`<$Text style={$styles.usual}>Your usual: {$summary}`; +} + export async function Home({ origin }: { origin: string }) { const menu = await getMenu(); const ordersUrl = origin + "/orders"; + const usual = await lastOrder(); return cs`( <$ScrollView @@ -110,7 +128,12 @@ contentContainerStyle={$styles.screen} > <$Text style={$styles.title}>Menu - <$Order menu={$menu} ordersUrl={$ordersUrl} /> + {${usual === null ? null : }} + <$Order + menu={$menu} + ordersUrl={$ordersUrl} + initialCounts={${usual ?? {}}} + /> )`; } ``` Your server keeps orders in memory, and it restarted when you saved, so place an order now, then reload the app: press `r` in the terminal running it. "Your usual" appears, and the order starts with it. - **`Usual` is a server component drawn inside a script,** with a braced splice: `{${}}`. It runs on your server, and the phone gets only what it returns. - **`${usual ?? {}}` is a splice of an expression,** for when a name isn't enough. It's the order's starting state. ## 6. Test it A test draws the screen as your app does, taps it, and checks what it shows. Create `server/Home.test.tsx`: ```tsx title="server/Home.test.tsx" import assert from "node:assert/strict"; import { afterEach, beforeEach, it } from "node:test"; import { render, screen } from "@testing-library/react"; import { userEvent } from "@testing-library/user-event"; import { Home } from "./Home.js"; import { saveOrder } from "./orders.js"; import { drawScreen } from "./test/drawScreen.js"; // The phone's `fetch`, answered here rather than by your server. const sent: string[] = []; const realFetch = globalThis.fetch; beforeEach(() => { sent.length = 0; globalThis.fetch = async (_url, init) => { sent.push(String(init?.body)); return new Response(); }; }); afterEach(() => { globalThis.fetch = realFetch; }); it("totals the order, and places it", async () => { render(await drawScreen()); await userEvent.click(screen.getByText("$4.50")); await userEvent.click(screen.getByText("$4.00")); assert.ok(screen.getByText("2 in your order · $8.50")); await userEvent.click(screen.getByText("Place order")); assert.ok(await screen.findByText("Ordered ✓")); assert.deepEqual(JSON.parse(sent[0]!), { "flat-white": 1, cortado: 1 }); }); it("starts from the usual", async () => { await saveOrder({ "flat-white": 2 }); render(await drawScreen()); assert.ok(screen.getByText("Your usual: 2 × Flat white")); assert.ok(screen.getByText("2 in your order · $9.00")); }); ``` `npm test` runs it. The phone's `fetch` is answered in the test, so placing an order doesn't need your server; the usual does, through `saveOrder`. [Testing screens](/docs/testing) explains `drawScreen`, and what a test doesn't cover. ## What you learned | You wrote | It's a | It runs on | | -------------------------------------------- | --------------------------------------- | -------------------- | | `async function Home()` | Server component | Your server | | `` cs`…` `` | Client script | The phone | | `$menu`, `${usual ?? {}}` | Splice: a server value, written as data | Crosses to the phone | | `` cs`(props) => …` ``, used as `<$Order />` | Client component | The phone | | `{${}}` | A server component inside a script | Your server | Everything you changed lives in `server/`. Deploy your server, and every user has the new screen the next time they open it, without an app release. And the server builds each screen per request, so `lastOrder` could read the user's account, A/B group or location. Next, **[Thinking in Backtick](/docs/thinking-in-backtick)** explains the model behind these steps. --- # Thinking in Backtick What runs on your server, what runs on the phone, and what crosses between them: server components, splices, client components, and how they compose. A Backtick screen is one file with code for two places: your server and the phone. Three rules say which code runs where: 1. **A server component runs on your server,** for every request. 2. **A client script, `` cs`…` ``, runs on the phone.** 3. **A splice, `$name`, is the only way a value crosses** from one to the other. ## One screen, both sides ```tsx title="server/Home.tsx" import { cs } from "@backtickjs/core"; import { useState } from "@backtickjs/react"; import { Pressable, ScrollView, Text } from "@backtickjs/react-native"; import { db, type Order, type User } from "./db.js"; // A client component: it runs on the phone, with state of its own. const ReorderButton = cs`(props: { order: Order }) => { const [added, setAdded] = $useState(false); return ( <$Pressable onPress={() => setAdded(true)}> <$Text>{added ? "Added ✓" : "Reorder " + props.order.name} ); }`; // A server component: it runs on your server, for every request. export async function Home({ user }: { user: User }) { const usual = await db.usualOrder(user.id); return cs`( <$ScrollView> <$Text>Good morning, {$user.name} <$ReorderButton order={$usual} /> )`; } ``` Read it by where each part runs: - **On your server: `Home`, and `await db.usualOrder(...)`.** It reads its data where the data lives. Queries, secrets and server-only packages stay on your server. - **Crossing as data: `$user` and `$usual`.** Each splice is a value `Home` has, written into the screen. In the script, it has its server type. - **On the phone: the script `Home` returns, and `ReorderButton`.** They draw the screen, keep state and handle taps. Your server bundles this screen for each request, and your app runs the bundle. [How it works](/docs/how-it-works) shows what the phone receives. ## Server components ```tsx title="server/ProductScreen.tsx" import { cs } from "@backtickjs/core"; import { Text, View } from "@backtickjs/react-native"; import type { Catalog } from "./catalog.js"; // A server component: a function of its props, run on your server for every // request. Its props stay on your server, so they can be anything. export async function ProductScreen({ id, catalog, }: { id: string; catalog: Catalog; }) { const product = await catalog.find(id); if (product === undefined) { return cs`<$Text>This product is gone.`; } const name = product.name; const inStock = product.stock > 0; return cs`( <$View style={{ padding: 24, gap: 8 }}> <$Text style={{ fontSize: 28, fontWeight: "bold" }}>{$name} <$Text>{$inStock ? "In stock" : "Sold out"} )`; } ``` - **Props stay on your server, so they can be anything:** a database client, a class instance like `Catalog`, a request. Only what you splice crosses. - **Decide on your server.** `ProductScreen` returns a different script for a product that's gone. The phone gets only the screen the decision led to. - **It has no state, and no hooks.** It runs once per request and returns. Calling `useState` in it is a type error: "This expression is not callable". State goes in a [client component](#client-components). ## Splices A splice carries a value from your server into a script. `$name` splices the variable `name`; `${expression}` splices any expression, as `${user.name}`. Both are evaluated on your server, when the `` cs`…` `` is. A splice can carry what can be written as data, and scripts: - Strings, numbers, bigints, booleans, `null` and `undefined` - Plain objects and arrays of those - Scripts: `` cs`…` `` values, client components and client functions - What the adapters export: `$View`, `$useState` and the rest Functions and class instances can't cross, because they're code and state on your server. TypeScript stops them where you write the splice: ```tsx title="server/Signup.tsx" import { cs } from "@backtickjs/core"; import { Pressable, Text } from "@backtickjs/react-native"; export async function Signup() { const opensAt = new Date(2026, 9, 6); const track = () => console.log("signup"); return cs`( // @ts-expect-error: a function on your server can't cross to the phone. <$Pressable onPress={$track}> {/* @ts-expect-error: nor can a class instance, a Date included. */} <$Text>Opens {$opensAt.toDateString()} )`; } ``` ```text Argument of type '() => void' is not assignable to parameter of type 'Spliceable'. Argument of type 'Date' is not assignable to parameter of type 'Spliceable'. ``` The fix is to cross what each one is for: the date as a string, and the handler as a client function, which runs on the phone. ```diff title="server/Signup.tsx" @@ -2,14 +2,12 @@ import { Pressable, Text } from "@backtickjs/react-native"; export async function Signup() { - const opensAt = new Date(2026, 9, 6); - const track = () => console.log("signup"); + const opensAt = new Date(2026, 9, 6).toDateString(); + const track = cs`() => console.log("signup")`; return cs`( - // @ts-expect-error: a function on your server can't cross to the phone. <$Pressable onPress={$track}> - {/* @ts-expect-error: nor can a class instance, a Date included. */} - <$Text>Opens {$opensAt.toDateString()} + <$Text>Opens {$opensAt} )`; } ``` Forget the `$`, and the script looks for a global by that name: "Property 'total' does not exist on type 'GlobalThis'". A script sees its own code and the phone's globals, never the file around it. ### A splice takes the whole value `$user.name` splices `user`, the whole object, then reads `name` on the phone. Everything in `user` crosses, including the email the screen never shows: ```tsx title="server/Whole.tsx" import { cs } from "@backtickjs/core"; import { Text } from "@backtickjs/react-native"; import type { User } from "./account.js"; // `$user.name` splices `user`, the whole object, and reads its name on the // phone: the email crosses too. export async function Greeting({ user }: { user: User }) { return cs`<$Text>Hello, {$user.name}`; } ``` In the bundle the phone receives, the splice is the whole object: ```js const $thunk1 = () => ({ id: "u1", name: "Ada", email: "ada@example.com" }); ``` To send only the name, splice the expression: `${user.name}`. The same goes for anything private in an object: splice what the screen needs, not what holds it. ### Optional values Each `$offer` in a script is its own splice, so TypeScript doesn't narrow one by checking another. In `{$offer && <$Banner offer={$offer} />}`, the second `$offer` could still be `null` as far as TypeScript knows. Read the splice once into a constant, and check that: ```tsx title="server/Promo.tsx" import { cs } from "@backtickjs/core"; import { Text } from "@backtickjs/react-native"; type Offer = { title: string }; // Not every user has an offer. async function offerFor(userId: string): Promise { return userId === "u-7" ? { title: "2 for 1 cold brew" } : null; } const Banner = cs`(props: { offer: Offer }) => ( <$Text>{props.offer.title} )`; export async function Promo({ userId }: { userId: string }) { const offer = await offerFor(userId); return cs`{ const offer = $offer; return offer && <$Banner offer={offer} />; }`; } ``` ## Client components A client component is a script written as a function that takes props and returns JSX, as `ReorderButton` above. It runs on the phone, where it keeps state and handles taps, and another script draws it as a tag. ```tsx title="server/Cart.tsx" import { cs } from "@backtickjs/core"; import { useState } from "@backtickjs/react"; import { Text, View } from "@backtickjs/react-native"; import type { ReactNode } from "react"; import { Stepper } from "./Stepper.js"; type Line = { id: string; name: string }; // A client component that draws what it's given, as `children`. const Card = cs`(props: { title: string; children: ReactNode }) => ( <$View style={{ padding: 16, gap: 8, borderRadius: 12, backgroundColor: "#f4f4f5", }} > <$Text style={{ fontWeight: "600" }}>{props.title} {props.children} )`; // State on the phone, handed to each row's stepper. const CartLines = cs`(props: { lines: Line[] }) => { const [quantities, setQuantities] = $useState>({}); return ( <$View style={{ gap: 12 }}> {props.lines.map((line) => ( <$Card key={line.id} title={line.name}> <$Stepper value={quantities[line.id] ?? 1} onChange={(value) => setQuantities({ ...quantities, [line.id]: value }) } min={1} /> ))} ); }`; export async function Cart() { const lines: Line[] = [ { id: "l1", name: "Flat white" }, { id: "l2", name: "Croissant" }, ]; return cs`<$CartLines lines={$lines} />`; } ``` - **Import it like anything else.** `Stepper` is a client component in its own file, with `value`, `onChange` and `min` props; `Cart.tsx` imports it and draws `<$Stepper />`. - **Props are typed by its parameter.** A missing or wrong prop is a type error where the tag is written. - **Inside client code, props can be anything:** `onChange` is a function, `children` is JSX. Only what crosses from your server has to be data. - **Hooks go here,** as in any React component: `$useState`, `$useEffect` and the rest. - **Lists take `key`,** as in React. - **Write it as a function, and it keeps its state.** A function script is one function for the whole bundle, so React sees the same component on every render. [Scripts in depth](/docs/scripts-in-depth#reading-a-script) explains why an expression wouldn't. - **It's a tag in a script, never on your server.** Drawn on your server, it "does not have any construct or call signatures". ## Composing ```tsx title="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"} ); }`; // 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} ))} )`; } // 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} {$children} )`; } export async function ProductPage({ productId }: { productId: string }) { return cs`( <$ScrollView contentContainerStyle={{ padding: 24, gap: 24 }}> <$Text style={{ fontSize: 28, fontWeight: "bold" }}> Ceramic dripper <$LikeButton /> { ${(
)} } )`; } ``` Server and client components compose in both directions, in one file: - **A client component inside a script:** `<$LikeButton />`. - **A server component inside a script:** `{${}}`, 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:** `Section` takes `` as its children and draws them with `{$children}`. Its `children` is a `JSX.Element` when it's handed a server element, as here, and a `Client` when it's handed a script. A server component runs once per place it's drawn, as React renders an element at each place it stands. If two places need the same data, fetch it once and pass it down. ## Where code goes | You need | Write it as | | ------------------------------------------------------------- | --------------------------------------------------------------- | | Data from a database or an API, secrets, server-only packages | Code in a server component | | State, effects, taps, animation, device APIs | Code in a client script | | A server value on the phone | A splice: `$name`, or `${expression}` | | A piece of UI with its own props and state | A client component: `` cs`(props) => …` ``, used as `<$Name />` | | A function the phone calls | A client function: `` cs`(n: number) => …` `` | | A piece of UI built on your server, inside a script | A server component, spliced: `{${}}` | --- # React Native and packages Every component, API and hook of React Native and React, spliced into scripts with their own names and types, and any other package your app ships. ```tsx title="server/Store.tsx" import { cs } from "@backtickjs/core"; import { useEffect, useRef } from "@backtickjs/react"; import { Animated, Linking, Platform, Pressable, Text, useWindowDimensions, View, } from "@backtickjs/react-native"; import type { ReactNode } from "react"; // Fades its children in when it appears. const FadeIn = cs`(props: { children: ReactNode }) => { const opacity = $useRef(new $Animated.Value(0)).current; $useEffect(() => { $Animated .timing(opacity, { toValue: 1, duration: 400, useNativeDriver: true }) .start(); }, []); return <$Animated.View style={{ opacity }}>{props.children}; }`; type StoreInfo = { name: string; phone: string; address: string }; const StoreCard = cs`(props: StoreInfo) => { const { width } = $useWindowDimensions(); const maps = $Platform.OS === "ios" ? { label: "Open in Maps", url: "https://maps.apple.com/?q=" } : { label: "Open in Google Maps", url: "https://www.google.com/maps/search/?api=1&query=", }; return ( <$FadeIn> <$View style={{ padding: 24, gap: 12, flexDirection: width > 600 ? "row" : "column", }} > <$Text style={{ fontSize: 24, fontWeight: "bold" }}>{props.name} <$Pressable onPress={() => $Linking.openURL("tel:" + props.phone)}> <$Text>Call {props.phone} <$Pressable onPress={() => $Linking.openURL(maps.url + encodeURIComponent(props.address)) } > <$Text>{maps.label} ); }`; export async function Store() { const store: StoreInfo = { name: "Backtick Coffee", phone: "+15550100", address: "1 Main St, Portland", }; return cs`<$StoreCard {...$store} />`; } ``` `@backtickjs/react-native` and `@backtickjs/react` export every component, API and hook of React Native and React, with the same names and types. In a script, splice them with `$`: - **Components are tags:** `<$View>`, `<$Pressable>`, `<$ScrollView>`. - **Members are tags too:** `<$Animated.View>` reads `View` off `Animated`. - **APIs are values:** `$Platform.OS`, `$Linking.openURL(…)`, `$StyleSheet.create(…)`. - **Hooks are called in client components,** as in any React component: `$useState`, `$useEffect`, `$useRef`, `$useWindowDimensions`. The phone runs them with the React Native your app ships, so a screen uses the same version as the rest of your app. ## The platform is the phone's `$Platform.OS` and `$useWindowDimensions()` run on the phone, so they describe the device the screen is drawn on. A server component can't know them; ask in a client component, as `StoreCard` does. ## Any package your app ships A screen can use any package your app ships. `createImport` names an export of a package, typed as the package types it, and splices like anything else: `$impactAsync()`, `<$LinearGradient>`. ```ts title="server/expo.ts" import { createImport } from "@backtickjs/core"; import type { impactAsync as ImpactAsync } from "expo-haptics"; import type { LinearGradient as ExpoLinearGradient } from "expo-linear-gradient"; // What the app provides from these packages, typed as the packages type // them, for any script to splice. export const impactAsync = createImport({ name: "impactAsync", from: "expo-haptics", version: "~57.0.0", }); export const LinearGradient = createImport({ name: "LinearGradient", from: "expo-linear-gradient", version: "~57.0.0", }); ``` A package a screen uses has to be in three places: **1. The screen,** spliced as any value or component: ```tsx title="server/Home.tsx" import { cs } from "@backtickjs/core"; import { Pressable, Text } from "@backtickjs/react-native"; import { impactAsync, LinearGradient } from "./expo.js"; export async function Home() { return cs`( <$LinearGradient colors={["#7c3aed", "#0891b2"]} style={{ flex: 1, padding: 24 }} > <$Pressable onPress={() => $impactAsync()}> <$Text style={{ color: "#fff", fontSize: 24 }}>Tap to feel it )`; } ``` **2. Your server,** in the versions the app provides: ```tsx title="server/index.tsx" import { createServer } from "node:http"; import { createRequire } from "node:module"; import { bundler } from "@backtickjs/bundler"; import { Home } from "./Home.js"; // The packages the app provides to screens, each at the version it's built // with. const require = createRequire(import.meta.url); const packageVersions = { react: require("react/package.json").version, "react-native": require("react-native/package.json").version, "expo-haptics": require("expo-haptics/package.json").version, "expo-linear-gradient": require("expo-linear-gradient/package.json").version, }; createServer(async (request, response) => { const bundle = await bundler.build({ input: , packageVersions }); response.setHeader("content-type", "text/javascript"); response.end(bundle.generate({ format: "cjs" }).code); }).listen(3000); ``` **3. Your app,** in the modules a screen may require, handed to `evaluate`: ```tsx title="App.tsx" import { evaluate } from "@backtickjs/react-native-client"; import * as ExpoHaptics from "expo-haptics"; import * as ExpoLinearGradient from "expo-linear-gradient"; import * as React from "react"; import * as JSXRuntime from "react/jsx-runtime"; import * as ReactNative from "react-native"; // What a screen may require: the packages this app was built with. export const modules = { react: React, "react/jsx-runtime": JSXRuntime, "react-native": ReactNative, "expo-haptics": ExpoHaptics, "expo-linear-gradient": ExpoLinearGradient, }; export async function fetchScreen(url: string): Promise { const response = await fetch(url); return evaluate(await response.text(), modules) as React.ReactNode; } ``` Install the package in your app as usual, `npx expo install expo-haptics`, so it's built into the app and its native code ships with it. ## Versions are checked `createImport`'s `version` is the range of the package the export works with. When a screen is bundled, the bundler checks it against the version your server says the app provides, and refuses a screen the app can't run, before anything reaches the phone. [Errors](/docs/errors#cant-import--from--the-client-provides-) has the messages. Serving apps of several versions, give each its own `packageVersions`, and each gets a screen it can run. --- # Testing screens Draw a screen in a test as your app does, tap it, and check what it shows. ```tsx title="server/Order.test.tsx" import assert from "node:assert/strict"; import { it } from "node:test"; import { render, screen } from "@testing-library/react"; import { userEvent } from "@testing-library/user-event"; import { Order } from "./Order.js"; import { drawScreen } from "./test/drawScreen.js"; it("counts each item on its own", async () => { render(await drawScreen()); await userEvent.click(screen.getByText("Flat white: 0")); assert.ok(screen.getByText("Flat white: 1")); assert.ok(screen.getByText("Latte: 0")); }); ``` A test draws a screen the way your app does, then taps it and checks what it shows. A new React Native project has one, `server/Home.test.tsx`, and runs it with `npm test`. ## What a test runs `drawScreen` builds the screen as your server does, and runs it as your app does, with React Native's web build standing in for React Native: ```ts title="server/test/drawScreen.ts" import { createRequire } from "node:module"; import { bundler } from "@backtickjs/bundler"; import type { Spliceable } from "@backtickjs/core"; import { evaluate } from "@backtickjs/react-native-client"; import * as React from "react"; import * as JSXRuntime from "react/jsx-runtime"; import * as ReactNativeWeb from "react-native-web"; // What the app provides, as `server/index.tsx` and `App.tsx` say, with React // Native's web build standing in for React Native. Add a package here when // your app provides one. const require = createRequire(import.meta.url); const packageVersions = { react: require("react/package.json").version, "react-native": require("react-native/package.json").version, }; const modules = { react: React, "react/jsx-runtime": JSXRuntime, "react-native": ReactNativeWeb, }; // A screen, bundled as your server bundles it and run as your app runs it, // for Testing Library to draw. export async function drawScreen(input: Spliceable): Promise { const bundle = await bundler.build({ input, packageVersions }); const { code } = bundle.generate({ format: "cjs" }); return evaluate(code, modules) as React.ReactNode; } ``` Testing Library draws it into [jsdom](https://github.com/jsdom/jsdom), a document that runs in Node. Here's the screen the test above draws. `Order` is a server component, and each `Stepper` keeps its own count on the phone: ```tsx title="server/Order.tsx" import { cs } from "@backtickjs/core"; import { useState } from "@backtickjs/react"; import { Pressable, Text, View } from "@backtickjs/react-native"; // A client component: its count lives on the phone. const Stepper = cs`({ name }: { name: string }) => { const [count, setCount] = $useState(0); return ( <$Pressable onPress={() => setCount(count + 1)}> <$Text> {name}: {count} ); }`; // A server component: what to order comes from your server. export async function Order({ items }: { items: string[] }) { return cs`( <$View> {$items.map((name) => ( <$Stepper key={name} name={name} /> ))} )`; } ``` ## Tapping `userEvent.click` taps what it's given, as a finger would. A `Pressable` answers it as it answers a tap, and the client component's state changes as it would on the phone. Each check reads what the screen shows, so the test passes or fails on what a user would see. ## Data from your server A server component runs in the test as it runs on your server, so it reads its data the same way. Give it test data through its props, as the test gives `Order` its items, or point what it reads at a test database. ## Packages your app provides When your app provides another package, add it to both lists in `drawScreen.ts`: its version to `packageVersions`, as `server/index.tsx` does, and its module to `modules`, as `App.tsx` does. A package with native code, like `expo-haptics`, has nothing to run in Node. Give `modules` a stand-in with the exports your screens use: ```ts const modules = { react: React, "react/jsx-runtime": JSXRuntime, "react-native": ReactNativeWeb, "expo-haptics": { impactAsync: async () => {} }, }; ``` ## What a test doesn't cover React Native's web build isn't React Native. A test checks what your screen shows and how it answers taps, but not: - **Native modules,** which a test replaces with stand-ins. - **Layout and platform behaviour,** like safe areas and keyboard handling. - **Native animation,** which the web build runs in JavaScript. Try those on a phone. ## Running `npm test` runs every `server/**/*.test.tsx` with Node's test runner: ```sh node --import ./server/test/setup.mjs --import @backtickjs/node-plugin --test "server/**/*.test.tsx" ``` The test runner compiles scripts without type-checking them, so a test can pass on a screen with a type error. Run `npm run typecheck` too, as CI should. `npm run reset-project` rewrites `server/Home.test.tsx` to match its blank screen. --- # Errors Every message Backtick can show you, by who says it, with its fix. Every message Backtick can show you, by who says it, with its fix. Your editor and `npm run typecheck` report the first two groups; the third is thrown by your server, per request, before anything reaches the phone; the last by your app. ## From TypeScript ### Argument of type '…' is not assignable to parameter of type 'Spliceable' The splice holds something that can't cross: a function, a class instance, a `Date`. Splice what it's for instead: a `Date` as a string, a function as a client function, `` cs`(n: number) => …` ``. See [what can cross](/docs/thinking-in-backtick#splices). ### Property '…' does not exist on type 'GlobalThis' A script used a name from your server without a `$`. Splice it: `$total`, not `total`. See [splices](/docs/thinking-in-backtick#splices). ### This expression is not callable. Type 'Client<…>' has no call signatures A hook, or another client value, was called in a server component. Hooks belong in a [client component](/docs/thinking-in-backtick#client-components). ### JSX element type '…' does not have any construct or call signatures A client component was drawn as a tag on your server. Draw it in a script: `` cs`<$Card />` ``. See [client components](/docs/thinking-in-backtick#client-components). ## From the compiler These are reported in the editor, and thrown when your server loads the file. ### `cs` was not compiled. Is @backtickjs set up for this project? Your server ran a script without the plugin that compiles them. Start it with `node --import @backtickjs/node-plugin`, or on Bun preload `@backtickjs/bun-plugin`. A new project's `npm start` already does. See [How it works](/docs/how-it-works#1-compiled-with-your-server). ### A `cs` client script is one expression or one block Statements need braces: `` cs`{ a(); b(); }` ``. See the [three shapes](/docs/scripts-in-depth#three-shapes). ### This `${…}` is in text, not code, so it isn't spliced A `${…}` inside a string or a comment in a script is text to the phone. As an element's child, write it in braces: `{${…}}`. ### A `cs` client script can't hold a template literal A script is a template literal itself, so `` \` `` can't start one inside it. Build the string with `+`: `n + "%"`, not `` `${n}%` ``. In a string or a comment, `` \` `` is a backtick, and fine. ### `$`-prefixed names are reserved for unbraced splices in a `cs` client script A script declared a name starting with `$`, as `const $label = …`. Those are splices; name it without the `$`. ### Can't splice `$name` unbraced: a `$`-prefixed host binding splices with braces A server variable whose own name starts with `$`, spliced as `$$name`. Write `${$name}`. ### A tag splices a host value by its name, e.g. `<$Card>`, not with `${…}` A tag was written as `<${Card}>`. Write `<$Card>`. ### `` is drawn by the client, so it belongs in a script A React Native tag was drawn on your server, outside a script. Draw it in one: `` cs`<$View>…` ``. ## From the bundler Thrown by `bundler.build`, for the screen it was building. ### Can't import `…` from "…": the client provides … A script uses a package missing from `packageVersions`. Add it there, at the version the app is built with, and to the app's modules. See [the three places a package goes](/docs/react-native-and-packages#any-package-your-app-ships). ### Can't import `…` from "…": it needs …, and the client provides … The app was built with a version of the package outside the range in `createImport`. Widen the range if the export works with that version, or serve that app a screen that doesn't use it. ### Can't splice the host function `…`: it's host code, and only runs on the host A function reached a splice, perhaps through `any`. Write the phone's code as a script, `` cs`(n: number) => …` ``. A server component is drawn with a tag in a braced splice: `{${}}`. ### Can't splice this `…` instance: only plain objects cross into a client script A class instance reached a splice. Cross its data as a plain object, `{ title: todo.title }`, or build it on the phone with a client function. ### Can't splice a symbol A symbol reached a splice, through `any`. Splice its key as a string, and make the symbol in a script: `` cs`Symbol.for($key)` ``. ### Can't splice a value that contains itself A value refers back to itself, such as a tree whose nodes point at their parents. Splice a copy without the back references. ### `…` is a client component, so it can't be a tag on the host A client component, `` cs`(props) => …` ``, was drawn as a tag on your server. Draw it in a script: `` cs`<$Card />` ``. ## From your app ### The bundle requires "…", which this app doesn't provide The bundle uses a package missing from the `modules` your app hands to `evaluate`. Add it, and install it in the app so its native code ships. If your server shouldn't have sent it, check its `packageVersions` for this app. See [the three places a package goes](/docs/react-native-and-packages#any-package-your-app-ships). --- # How it works Compiled when your server loads, checked by TypeScript, bundled for each request, run by your app: what each step does, and the packages that do it. A screen goes through four steps: compiled when your server loads its files, checked by TypeScript, bundled for each request, and run by your app. A new project has all four set up; this page is for when you want to know what each one did, or to set up a server by hand. ## 1. Compiled with your server Your server runs with a loader, `@backtickjs/node-plugin` on Node or `@backtickjs/bun-plugin` on Bun. As it loads each `.tsx` file, it compiles each `` cs`…` `` to plain JavaScript, with the compile step your framework names in `package.json`: ```json { "backtick": { "plugins": ["@backtickjs/react/plugin"] } } ``` A compiled script is a module: a function of its splices, which reads each splice where the script wrote it. A script compiles once, however many requests use it. The loaders' READMEs, [node-plugin](https://github.com/backtickjs/backtick/tree/main/packages/node-plugin) and [bun-plugin](https://github.com/backtickjs/backtick/tree/main/packages/bun-plugin), have their flags. To build your server ahead of time instead, [tspatch-plugin](https://github.com/backtickjs/backtick/tree/main/packages/tspatch-plugin) does the same with `tspc`. ## 2. Checked by TypeScript The code inside every script, and every value crossing into it, is checked as TypeScript. A new project's `npm run typecheck` checks your app and your server: ```sh tsc --noEmit && backtick-tsc -p server --noEmit ``` `backtick-tsc` is `tsc` that understands client scripts: it takes `tsc`'s arguments, and reports errors inside scripts at the line and column you wrote. Run it in CI as you would `tsc`. The server's `tsconfig.json` needs two settings, which the template sets and the [react-native adapter's README](https://github.com/backtickjs/backtick/tree/main/packages/react-native#setup) explains. [Backtick for VS Code](https://marketplace.visualstudio.com/items?itemName=backtickjs.backtick-vscode) highlights scripts as TSX, colours splices, and shows errors, completions and hover information inside scripts. ## 3. Bundled for each request Your server bundles a screen when the app asks for it: ```tsx title="server/index.tsx" import { createServer } from "node:http"; import { bundler } from "@backtickjs/bundler"; import { db } from "./db.js"; import { Home } from "./Home.js"; // The versions of React and React Native your app ships. const packageVersions = { react: "19.2.3", "react-native": "0.86.3" }; createServer(async (request, response) => { if (request.url === "/screens/home") { const user = await db.userFor(request.headers.authorization); const bundle = await bundler.build({ input: , packageVersions, }); const { code } = bundle.generate({ format: "cjs" }); response.setHeader("content-type", "text/javascript"); response.end(code); return; } response.statusCode = 404; response.end(); }).listen(3000); ``` `bundler.build` runs the server components in `input`, then writes what they returned as one JavaScript file. For a request from Sam, the screen in [Thinking in Backtick](/docs/thinking-in-backtick#one-screen-both-sides) becomes this bundle. It's generated by the docs' tests, so it's exactly what the bundler writes: ```js title="server/Home.bundle.js" "use strict"; const { ScrollView: $i0 } = require("react-native"); const { Text: $i1 } = require("react-native"); const { useState: $i2 } = require("react"); const { Pressable: $i3 } = require("react-native"); const $module0 = require("react/jsx-runtime"); const $modules = { "25wlsdb7ez0q4:20:9": (module, exports, require) => { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const jsx_runtime_1 = require("react/jsx-runtime"); exports.default = ($splice0, $splice1, $splice2, $splice3, $splice4) => ($ScrollView => /*#__PURE__*/ (0, jsx_runtime_1.jsxs)($ScrollView, { children: [($Text => /*#__PURE__*/ (0, jsx_runtime_1.jsxs)($Text, { children: ["Good morning, ", $splice2().name] }))($splice1()), ($ReorderButton => /*#__PURE__*/ (0, jsx_runtime_1.jsx)($ReorderButton, { order: $splice4() }))($splice3())] }))($splice0()); }, "25wlsdb7ez0q4:7:22": (module, exports, require) => { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const jsx_runtime_1 = require("react/jsx-runtime"); exports.default = ($splice0, $splice1, $splice2) => props => { const [added, setAdded] = $splice0()(false); return ($Pressable => /*#__PURE__*/ (0, jsx_runtime_1.jsx)($Pressable, { onPress: () => setAdded(true), children: ($Text => /*#__PURE__*/ (0, jsx_runtime_1.jsx)($Text, { children: added ? "Added ✓" : "Reorder " + props.order.name }))($splice2()) }))($splice1()); }; }, }; const $exports = { "react/jsx-runtime": $module0, }; const $require = (id) => { if (!(id in $exports)) { const module = { exports: {} }; $exports[id] = module.exports; $modules[id](module, module.exports, $require); $exports[id] = module.exports; } return $exports[id]; }; const $cs0 = $require("25wlsdb7ez0q4:20:9").default; const $cs1 = $require("25wlsdb7ez0q4:7:22").default; const $thunk0 = () => ($i0); const $thunk1 = () => ($i1); const $thunk2 = () => ({ id: "u-7", name: "Sam" }); const $thunk3 = () => ($i2); const $thunk4 = () => ($i3); const $function5 = $cs1($thunk3, $thunk4, $thunk1); const $thunk6 = () => ($function5); const $thunk7 = () => ({ id: "o-1042", name: "Flat white" }); module.exports = ($cs0($thunk0, $thunk1, $thunk2, $thunk6, $thunk7)); ``` From the top: - **The packages the app provides,** required by name: `react-native`, `react`, `react/jsx-runtime`. The bundle requires them rather than including them, and each is checked against the versions in `packageVersions`. - **The module table:** each script's compiled module, once, however many times it's used. Their JSX is React's `jsx` calls. - **The bundle's constants.** Each splice is a small function, `$thunk`, that the script calls when it reads the splice: Sam's name and order are there, as data. `ReorderButton` is `$function5`, made once for the whole bundle, so React keeps its state across renders. The same code anywhere in the bundle is the same constant. - **The root:** the screen, as the call of its script. Nothing of `Home` is in the bundle, and `db` isn't either: the phone gets results, never the code that produced them. Only what a request drew is in its bundle. `generate` writes it as CommonJS, `"cjs"`, for React Native, or as an ES module, `"es"`, for a web page. It can also write a source map into your server's files: `"inline"` appends it to the code, `"hidden"` returns it apart, for reading the app's stack traces on your server. ```ts title="server/sourcemap.ts" import type { Bundle } from "@backtickjs/bundler"; // The bundle for the app, and its map kept on the server, for reading the // app's stack traces against your server's files. export function write(bundle: Bundle, maps: Map, id: string) { const { code, map } = bundle.generate({ format: "cjs", sourcemap: "hidden" }); maps.set(id, map!); return code; } ``` A map holds your files' names and lines, never their code. The [bundler's README](https://github.com/backtickjs/backtick/tree/main/packages/bundler) has every option. ## 4. Run by your app Your app fetches the bundle like any request, and runs it with `evaluate` from `@backtickjs/react-native-client`, handing it the packages it ships. What the bundle exports is the screen, a React node the app draws: ```tsx title="App.tsx" import { evaluate } from "@backtickjs/react-native-client"; import * as React from "react"; import { Suspense, use, useState } from "react"; import * as JSXRuntime from "react/jsx-runtime"; import * as ReactNative from "react-native"; import { ActivityIndicator } from "react-native"; // What a screen may require: the packages your app is built with. const modules = { react: React, "react/jsx-runtime": JSXRuntime, "react-native": ReactNative, }; async function fetchScreen(url: string): Promise { const response = await fetch(url); return evaluate(await response.text(), modules) as React.ReactNode; } export default function App() { const [screen] = useState(() => fetchScreen("https://api.example.com/screens/home"), ); return ( }> ); } function Screen({ screen }: { screen: Promise }) { return use(screen); } ``` Both files are what `create-backtick-app` sets up, without its development extras. Fetching is your app's, and so is when: on launch, on focus, on a pull to refresh. `evaluate` runs the code it's given, as `eval` does, so fetch bundles only from your own server, over HTTPS. Because the phone runs code your server wrote, a deploy of your server is a release of the screen: the next time someone opens it, they get the new one. ## Packages What a React Native project uses, each linked to its README: | Package | What it is | | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------- | | [`@backtickjs/core`](https://github.com/backtickjs/backtick/tree/main/packages/core) | The `cs` tag, and the types for what crosses | | [`@backtickjs/react-native`](https://github.com/backtickjs/backtick/tree/main/packages/react-native) | React Native's components and APIs, for scripts | | [`@backtickjs/react`](https://github.com/backtickjs/backtick/tree/main/packages/react) | React's hooks and APIs, for scripts | | [`@backtickjs/bundler`](https://github.com/backtickjs/backtick/tree/main/packages/bundler) | Runs server components and bundles a screen | | [`@backtickjs/react-native-client`](https://github.com/backtickjs/backtick/tree/main/packages/react-native-client) | `evaluate`: runs a bundle in your app | | [`@backtickjs/node-plugin`](https://github.com/backtickjs/backtick/tree/main/packages/node-plugin) | Compiles scripts as your Node server loads them | | [`@backtickjs/tsc`](https://github.com/backtickjs/backtick/tree/main/packages/tsc) | `backtick-tsc`: type-checks scripts and splices | | [`@backtickjs/prettier-plugin`](https://github.com/backtickjs/backtick/tree/main/packages/prettier-plugin) | Formats the code inside scripts | The [repository's README](https://github.com/backtickjs/backtick#packages) lists them all, the web adapters and Bun included. --- # Scripts in depth A script's three shapes, when its code runs, scripts inside data, and what a splice checks. To your server, a script is a value, a `Client`: something the phone will compute as a `T`. This page is what follows from that, for when a screen does something you didn't expect. ## Three shapes ```ts title="server/shapes.ts" import { type Client, cs } from "@backtickjs/core"; // An expression is what it computes. export const greeting: Client = cs`new Date().getHours() < 12 ? "Good morning" : "Good afternoon"`; // A block is what it returns. export const meal: Client = cs`{ const hour = new Date().getHours(); return hour < 11 ? "Breakfast" : "Lunch"; }`; // A function is the function. export const double: Client<(n: number) => number> = cs`(n: number) => n * 2`; ``` A script is an expression, a block of statements, or a function. An expression is a `Client` of its value; a block, of what it returns; a function, of the function itself. A function that returns JSX is a client component. TypeScript infers each type; the annotations above only show them. Your server can't read the `T`, which doesn't exist until the phone runs the script. It can splice the script into another, pass it around, and return it from a server component. Annotate a function that builds scripts with `Client`, taking the phone's number and giving back the phone's string: ```ts title="server/formatted.ts" import { type Client, cs } from "@backtickjs/core"; // A script built from another: the phone computes `amount`, then formats it. export function formatted(amount: Client): Client { return cs`"$" + $amount.toFixed(2)`; } export const total = formatted(cs`4.5 + 4`); ``` ## Reading a script How a splice of a script behaves follows from its shape, as the same code written by hand in TypeScript would: - **A function is one function.** `$logTap` is the same function at every read, and its body runs each time it's called. - **An expression or a block is code, run each time it's read.** Reading `$logVisit` twice runs it twice. ```tsx title="server/Log.tsx" import { cs } from "@backtickjs/core"; import { Text } from "@backtickjs/react-native"; // A block: its code runs each time it's read. const logVisit = cs`{ console.log("visit"); }`; // A function: one function, whose body runs each time it's called. const logTap = cs`(what: string) => console.log("tap " + what)`; export async function Home() { return cs`{ $logVisit; $logVisit; $logTap("a"); $logTap("b"); return <$Text>Logged; }`; } ``` This is why a client component is written as a function: one function for the bundle, so React sees the same component on every render and keeps its state. A component made by an expression or a block is new code at each read, so React remounts it on every render of its parent, as it would one defined inside a render function. A component made by a call, such as `memo(…)`, is made once where React would make it, in `useMemo`: ```tsx title="server/Rows.tsx" import { cs } from "@backtickjs/core"; import { memo, useMemo, useState } from "@backtickjs/react"; import { Pressable, Text, View } from "@backtickjs/react-native"; // A client component, written as a function: one component for the bundle. const Row = cs`(props: { label: string }) => { const [count, setCount] = $useState(0); return ( <$Pressable onPress={() => setCount(count + 1)}> <$Text> {props.label} {count} ); }`; export const List = cs`() => { // memo(…) is a call: made once per list, so it is one component too. const MemoRow = $useMemo(() => $memo($Row), []); const [taps, setTaps] = $useState(0); return ( <$View> <$Pressable onPress={() => setTaps(taps + 1)}> <$Text>list {taps} ); }`; export async function Home() { return cs`<$List />`; } ``` ## No template literals inside a script A script is a template literal itself, so it can't hold one: a backtick in its code is refused, with "A `cs` client script can't hold a template literal". Build the string with `+` instead: `n + "%"`, not `` `${n}%` ``. In a string or a comment, write a backtick as `` \` ``, as anywhere in a template literal. ## A script in data A splice can carry scripts inside data. On the phone, each `Client` in it is the `U` it computes; everything else is unchanged. `Spliced` names that type, and TypeScript applies it to every splice: ```ts title="server/menu.ts" import { type Client, cs, type Spliced } from "@backtickjs/core"; type Item = { name: string; price: number; // Computed on the phone, from its own clock. available: Client; }; export const menu: Item[] = [ { name: "Flat white", price: 4.5, available: cs`true` }, { name: "Croissant", price: 3, available: cs`new Date().getHours() < 11` }, ]; // On the phone, each `available` is the boolean its script computes. export type MenuOnPhone = Spliced; // { name: string; price: number; available: boolean }[] export const names = cs`$menu .filter((item) => item.available) .map((item) => item.name)`; ``` ## Client code handed to a server component A server component drawn inside a client component can be handed that component's local values, as client code: ```tsx title="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 ? "♥" : "♡"} ); }`; // A server component handed client code: its title is the phone's to compute. async function Section({ title }: { title: Client }) { return cs`( <$View> <$Text>{$title} <$Like /> )`; } // 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 {${(
)}} ); }`; export async function Home() { return cs`<$Cart />`; } ``` `` cs`count + " in your cart"` `` is a fragment of `Cart`'s code, reading its `count`. `Section` splices it as `$title`, and the phone computes it each time it's read, so the title follows the count. `Section` itself runs once, on your server: it decides the layout, and the phone fills in what depends on its state. ## Data, never code A spliced string arrives as a string, whatever it holds. A note that reads like code is still only text: ```tsx title="server/Data.tsx" import { cs } from "@backtickjs/core"; import { Text } from "@backtickjs/react-native"; // A splice is written into the bundle as data, never as code: whatever this // string holds, the phone shows it as text. export async function Note({ text }: { text: string }) { return cs`<$Text>{$text}`; } ``` For a note holding `"); require("fs").rmSync("/"); ("`, the bundle writes it escaped, inside quotes: ```js const $thunk1 = () => ("\"); require(\"fs\").rmSync(\"/\"); (\""); ``` Nothing a user types can become code on the phone. ## What a splice checks A splice checks a value member by member, so an object typed with an interface splices as one typed with a type alias does. `Spliceable` written by hand, in an annotation or a `satisfies`, matches an object through an index signature, which TypeScript never gives an interface: annotate with your own types and let the splice check them. Numbers include `NaN`, `Infinity` and `-0`, which arrive as themselves, as do bigints. A class whose members are all data looks like a plain object to TypeScript, so the bundler is what refuses it, when it builds the screen. A value that contains itself is refused too: a bundle writes each value out in full. [Errors](/docs/errors#from-the-bundler) has each message. The rest of `@backtickjs/core` is how Backtick's own pieces talk to each other; its [README](https://github.com/backtickjs/backtick/tree/main/packages/core#api) lists them. An app doesn't need it.