Splices
How a value crosses from your server into a client script, and what can cross.
import { cs } from "@backtickjs/core";import { Text, View } from "@backtickjs/react-native";import { account, type User } from "./account.js";export async function Profile({ user }: { user: User }) {const orders = await account.ordersFor(user.id);const points = await account.pointsFor(user.id);return cs`(<$View style={{ padding: 24, gap: 8 }}><$Text>{${user.name}}</$Text><$Text>{${orders.length}} orders</$Text><$Text>{$points} points</$Text></$View>)`;}
A splice carries a value from your server into a client script. Your server evaluates it when the script is made, and the bundle carries the result to the phone, as data.
Two forms
$namesplices the variablename:$points,$View.${expression}splices any expression:${user.name},${orders.length}.
Both are evaluated on your server, in order, when the cs`…` is.
What can cross
A splice can carry what can be written as data, and scripts:
- Strings, finite numbers, booleans,
nullandundefined - Plain objects and arrays of those
- Scripts: values, functions and client components made with
cs`…` - What the adapters export, as
$Viewor$useState
On the phone, each value has the type it had on your server, except a script:
a Client<T> arrives as the T it computes. That's why $formatPrice can be
called, and <$Counter /> drawn.
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>`;}
What the phone receives for a note holding code
"use strict";const { Text: $i0 } = require("react-native");const $module0 = require("react/jsx-runtime");const $modules = {"3gtesv8q899mn:7:9": (module, exports, require) => {"use strict";Object.defineProperty(exports, "__esModule", { value: true });const jsx_runtime_1 = require("react/jsx-runtime");exports.default = ($splice0, $splice1) => ($Text => /*#__PURE__*/ (0, jsx_runtime_1.jsx)($Text, {children: $splice1()}))($splice0());},};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("3gtesv8q899mn:7:9").default;module.exports = ($cs0(() => ($i0), () => ("\"); require(\"fs\").rmSync(\"/\"); (\"")));
The bundle writes the note escaped, inside quotes. Nothing a user types can become code on the phone.
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:
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}</$Text>`;}
What the phone receives
"use strict";const { Text: $i0 } = require("react-native");const $module0 = require("react/jsx-runtime");const $modules = {"chpeziv4ml4:8:9": (module, exports, require) => {"use strict";Object.defineProperty(exports, "__esModule", { value: true });const jsx_runtime_1 = require("react/jsx-runtime");exports.default = ($splice0, $splice1) => ($Text => /*#__PURE__*/ (0, jsx_runtime_1.jsxs)($Text, {children: ["Hello, ", $splice1().name]}))($splice0());},};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("chpeziv4ml4:8:9").default;module.exports = ($cs0(() => ($i0), () => ({ id: "u1", name: "Ada", email: "ada@example.com" })));
To send only the name, splice the expression: ${user.name}, as Profile
does. The same goes for anything private in an object: splice what the screen
needs, not what holds it.
What can't cross
Functions and class instances stay on your server: they're code, and state
the phone can't have. TypeScript stops them at the splice, with
… is not assignable to parameter of type 'Spliceable'. Cross what each one
is for instead: a Date as a string, a function as a client function,
cs`(n: number) => …`. Thinking in Backtick
has the fix in full.
Two more rules
- A script can't declare a name starting with
$. Those are splices, andconst $label = "Hi"is refused: "$-prefixed names are reserved for unbraced splices in acsclient script." ${…}in a script's text isn't a splice. In a string, a template literal or a comment, it's refused, since the phone would read it as text: "This${…}is in text, not code, so it isn't spliced."