Contexts
Mobile
A page that answers /tasks/:id behind an HTTP adapter answers it on a phone too. What changes is where the result goes: a browser replaces a document, a phone pushes a screen onto a stack. Everything before that last step is the code you already wrote.
#The whole difference, in two files
Open a web application's manifest next to a native one. This is not an abridged comparison: it is the entire difference between the two files.
import { Routing } from '@stone-js/router'
import { StoneApp } from '@stone-js/core'
import { UseReact } from '@stone-js/use-react'
import { Browser } from '@stone-js/browser-adapter'
@Routing()
@Browser() // where events come from
@UseReact() // what a resolved route becomes
@StoneApp({ name: 'acme' }, [domainBlueprint])
export class Application {}import { routerBlueprint } from '@stone-js/router'
import { useReactBlueprint } from '@stone-js/use-react'
import { defineStoneApp } from '@stone-js/core'
import { browserAdapterBlueprint } from '@stone-js/browser-adapter'
export const Application = defineStoneApp(
{ name: 'acme' },
[routerBlueprint, browserAdapterBlueprint, useReactBlueprint, domainBlueprint]
)Two decorators differ, or two blueprints. The router is the same, the domain is the same line, and nothing else moved. Then open the pages: handle and head are identical, character for character, because answering a route is not a platform question. Only render differs, and only in what it draws with.
@Page('/tasks/:id')
export class TaskScreen implements IPage<ReactIncomingEvent> {
constructor (private readonly tasks: TaskService) {}
// Identical on both platforms. Answering a route is not a platform question.
async handle (event: ReactIncomingEvent) {
return await this.tasks.find(event.get('id'))
}
head ({ data }) {
return { title: data.title }
}
// The only difference, and only in what it draws with.
render ({ data }) {
return <View><Text>{data.title}</Text></View>
}
}If the domain never named the web, it never has to un-name it to run on a phone.
#Why mobile is the case that proves the claim
Every context Stone.js supports asks the domain for nothing. That is easy to believe for the ones that look alike: a Node server and a Lambda both receive a request and return a response, so an adapter between them is unsurprising. Mobile is the case where the claim could have failed, and it is worth being precise about why.
On a phone, the bundler itself changes hands. The web build asks Vite to collect your pages and hands the result to Rollup; a native build hands everything to Metro, which resolves every import statically and has no notion of a glob. The rendering target changes too: there is no document to replace, so a resolved route becomes a screen on a stack rather than markup in a container. And navigation stops being a URL bar: it is a deep link, a gesture, a hardware button.
Three of the four dimensions change completely. Setup gains a different build, Integration gains a different adapter, Initialization renders into a different place. The fourth, the functional dimension, the part you wrote, does not move.
#What you install
Two packages, split along the same line as everywhere else in Stone.js: one captures causes, one renders resolutions.
npm i @stone-js/react-native-adapter @stone-js/use-react-native@stone-js/react-native-adapteris the Integration dimension: deep links, in-app navigation and the launch intent become incoming events.@stone-js/use-react-nativeis the renderer: a resolved route becomes a screen, and the screens form a stack.
Everything a page is made of, the @Page decorator, layouts, error pages, view providers, the hooks, comes from @stone-js/use-react-core, shared with the web renderer. There is no native variant of a page's logic to learn.
#One domain, two applications
The argument above is testable, and there is a starter that tests it. Scaffold monorepo-declarative and you get three packages: a domain that imports @stone-js/core and nothing else about a platform, a web application, and a native one.
npx @stone-js/create@latest acme --starter monorepo-declarativeacme/
├── packages/domain/ @acme/domain the entities and the behaviour
├── apps/web/ @acme/web @Browser + @UseReact
└── apps/mobile/ @acme/mobile @ReactNative + @UseReactNativeIts three test suites sit at three levels, and the cheapest one carries the most. The domain boots nothing and tests plain objects in milliseconds. The web application boots the real kernel and reads the HTML that came back. The mobile one boots the real kernel and asserts what landed on the navigation stack. The web and mobile suites make the same assertions about the same domain through two different contexts, which is the claim stated as code rather than as prose.
#What a phone genuinely adds
Being honest about a claim means naming what it does not cover. Three things about mobile are real work, and none of them is your domain.
#The build belongs to Expo
Metro and Expo own native bundling: Hermes, per-platform resolution, the native projects, the dev client. Stone.js does not offer a second opinion on any of it. What it does is answer the one question Metro cannot: which modules make up your application. Two lines in metro.config.js collect everything under app/ into real static imports before Metro runs, so adding a screen stays adding a file.
const { getDefaultConfig } = require('expo/metro-config')
const { withStone } = require('@stone-js/use-react-native/metro')
module.exports = withStone(getDefaultConfig(__dirname), __dirname)#Navigation is a platform feeling
A stack of screens with the platform's transitions, the swipe-back gesture and the hardware button cannot be imitated in JavaScript. The renderer ships a floor that works with nothing installed, and a real navigator you graduate to. Both are described on the native screens page.
#A device is a device
The fastest development loop for a native application is a browser tab: Expo serves it through react-native-web with Fast Refresh, and the same code then runs on a phone untouched. Use it for the domain, the navigation and most of the interface. Then use a device, because a browser covers the core primitives and not the camera, secure storage or a native gesture handler, and because performance in a tab says nothing about a phone.