Skip to content
Bullet

Getting started

Install Bullet and configure your dev server, bundler, and entry points

This guide walks you through adding Bullet to a React Native project so your app runs on Android, iOS, and the web from a single dev server embedded in an H3 application. By the end, you'll have a working react-native start command backed by Rspack and streaming React Native Web SSR.

When you finish, your project's key files look like this:

    • application.tsx
    • index.tsx
    • index.server.tsx
  • babel.config.cjs
  • react-native.config.cjs
  • rspack.config.ts
  • server.ts

Install Bullet

Add the Bullet server and configuration packages, along with the H3 and crossws runtime dependencies.

Register the CLI commands

Point the React Native CLI at Bullet's commands so react-native start and react-native bundle run through Bullet instead of Metro.

react-native.config.cjs
module.exports = {
	commands: require("@clutchify/bullet-core/command"),
};

Configure the bundler

Create rspack.config.ts with four named configurations: client and server for the web build, and android and ios for the native builds. Each builder takes an entry point and a configuration name.

rspack.config.ts
import {
	createClientConfiguration,
	createNativeConfiguration,
	createServerConfiguration,
} from "@clutchify/bullet-core";
import { defineConfig } from "@rspack/cli";
 
export default defineConfig([
	createClientConfiguration({ name: "client", entry: "./src/index.tsx" }),
	createServerConfiguration({ name: "server", entry: "./src/index.server.tsx" }),
	createNativeConfiguration({ name: "android", entry: "./src/index.tsx", platform: "android" }),
	createNativeConfiguration({ name: "ios", entry: "./src/index.tsx", platform: "ios" }),
]);

Configure Babel

Create babel.config.cjs. The SWC loader passes the target platform through the Babel caller, so you enable babel-plugin-react-native-web only for the web build and use the React Native preset everywhere.

babel.config.cjs
module.exports = (api) => {
	const platform = api.caller((caller) => caller?.platform);
 
	return {
		presets: ["module:@react-native/babel-preset"],
		plugins: [
			"module:babel-plugin-react-compiler",
			platform === "web" && "module:babel-plugin-react-native-web",
		].filter(Boolean),
	};
};

Create the development server

Create server.ts. It mounts bullet() into an H3 app, installs the crossws server plugin, and maps each environment name to the matching configuration name from rspack.config.ts. Bullet wires up the React Native DevTools, dev menu, and inspector sockets internally.

server.ts
import { bullet } from "@clutchify/bullet-server";
import { plugin as websocketPlugin } from "crossws/server";
import { H3 } from "h3";
import { serve } from "h3/node";
 
import configuration from "./rspack.config.ts";
 
const port = Number(process.env.REPACK_PORT ?? 8081);
const host = process.env.REPACK_HOST;
 
const bulletApplication = await bullet({
	configuration,
	mode: process.env.NODE_ENV === "production" ? "production" : "development",
 
	environments: {
		client: "client",
		server: "server",
 
		android: "android",
		ios: "ios",
	},
 
	development: { port, host },
});
 
const application = new H3().mount("", bulletApplication);
 
serve(application, { hostname: host, port, plugins: [websocketPlugin({})] });

Create your root component

Both entry points render the same component tree, so define it once in src/application.tsx. Application is your app's root component, and Document is the HTML shell used for web rendering.

src/application.tsx
import type { PropsWithChildren, ReactNode } from "react";
 
import { Text, View } from "react-native";
 
export function Application() {
	return (
		<View>
			<Text>Hello from Bullet</Text>
		</View>
	);
}
 
export type DocumentProps = PropsWithChildren<{ head?: ReactNode }>;
 
export function Document({ children, head }: DocumentProps) {
	return (
		<html>
			<head>
				<meta charSet="utf-8" />
				<meta content="width=device-width, initial-scale=1" name="viewport" />
				{head}
			</head>
			<body>{children}</body>
		</html>
	);
}

Add the client entry

Create src/index.tsx. It registers your app with AppRegistry for native platforms and hydrates the server-rendered HTML on the web.

src/index.tsx
import { StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { AppRegistry, Platform } from "react-native";
 
import { Application, Document } from "./application";
 
AppRegistry.registerComponent("main", () => Application);
 
if (Platform.OS === "web") {
	const { element } = AppRegistry.getApplication("main");
 
	hydrateRoot(
		document,
		<StrictMode>
			<Document>{element}</Document>
		</StrictMode>,
	);
}

Add the server entry

Create src/index.server.tsx. It uses createServerHandler to render your app to a stream on the server and injects the asset manifest so the client can hydrate.

src/index.server.tsx
import { createServerHandler } from "@clutchify/bullet-server/entry";
import { StrictMode } from "react";
import { renderToReadableStream } from "react-dom/server";
import { AppRegistry } from "react-native";
 
import { Application, Document } from "./application";
 
export default createServerHandler(async ({ manifest }) => {
	AppRegistry.registerComponent("main", () => Application);
	const { element, getStyleElement } = AppRegistry.getApplication("main", { initialProps: {} });
 
	const stream = await renderToReadableStream(
		<StrictMode>
			<Document head={<>{getStyleElement()}</>}>{element}</Document>
		</StrictMode>,
		{
			bootstrapScriptContent: `window.assetMap = ${JSON.stringify(manifest)};`,
			bootstrapScripts: Object.values(manifest).filter((item) => !item.endsWith(".map")),
		},
	);
 
	return new Response(stream, { headers: { "content-type": "text/html" } });
});

Start the development server

Run the React Native CLI. Bullet launches your server.ts, starts the Rspack compilers, and serves every platform from one process.

npx react-native start

Open the printed URL to see the web build, or launch the app on Android or iOS to load the native bundle.