Splices

How a value crosses from your server into a client script, and what can cross.

server/Profile.tsx
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

  • $name splices the variable name: $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, null and undefined
  • Plain objects and arrays of those
  • Scripts: values, functions and client components made with cs`…`
  • What the adapters export, as $View or $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:

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>`;
}
What the phone receives for a note holding code
server/Data.bundle.js
"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:

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}</$Text>`;
}
What the phone receives
server/Whole.bundle.js
"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, and const $label = "Hi" is refused: "$-prefixed names are reserved for unbraced splices in a cs client 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."