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
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:
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.
$logTapis 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
$logVisittwice runs it twice.
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:
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:
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:
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:
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.