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.
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><$Viewstyle={{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><$PressableonPress={() =>$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>readsViewoffAnimated. - 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>.
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:
import { cs } from "@backtickjs/core";import { Pressable, Text } from "@backtickjs/react-native";import { impactAsync, LinearGradient } from "./expo.js";export async function Home() {return cs`(<$LinearGradientcolors={["#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:
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:
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.