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<T>: 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

server/shapes.ts
import { type Client, cs } from "@backtickjs/core";
// An expression is what it computes.
export const greeting: Client<string> = cs`new Date().getHours() < 12
? "Good morning"
: "Good afternoon"`;
// A block is what it returns.
export const meal: Client<string> = 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<T>, taking the phone's number and giving back the phone's string:

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<number>): Client<string> {
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.
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</$Text>;
}`;
}

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:

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}
</$Text>
</$Pressable>
);
}`;
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}</$Text>
</$Pressable>
<MemoRow label="row" />
</$View>
);
}`;
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<U> in it is the U it computes; everything else is unchanged. Spliced<T> names that type, and TypeScript applies it to every splice:

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<boolean>;
};
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<Item[]>;
// { 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:

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 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:

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}</$Text>`;
}

For a note holding "); require("fs").rmSync("/"); (", the bundle writes it escaped, inside quotes:

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 has each message.

The rest of @backtickjs/core is how Backtick's own pieces talk to each other; its README lists them. An app doesn't need it.