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.

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}</$Animated.View>;
}`;
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}</$Text>
<$Pressable onPress={() => $Linking.openURL("tel:" + props.phone)}>
<$Text>Call {props.phone}</$Text>
</$Pressable>
<$Pressable
onPress={() =>
$Linking.openURL(maps.url + encodeURIComponent(props.address))
}
>
<$Text>{maps.label}</$Text>
</$Pressable>
</$View>
</$FadeIn>
);
}`;
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>.

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<typeof ImpactAsync>({
name: "impactAsync",
from: "expo-haptics",
version: "~57.0.0",
});
export const LinearGradient = createImport<typeof ExpoLinearGradient>({
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:

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

2. Your server, in the versions the app provides:

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: <Home />, 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:

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<React.ReactNode> {
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 has the messages. Serving apps of several versions, give each its own packageVersions, and each gets a screen it can run.