Stone.jsDocs
Paradigm

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.

declarativeimperative
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]
)
import { Routing } from '@stone-js/router'
import { StoneApp } from '@stone-js/core'
import { UseReactNative } from '@stone-js/use-react-native'
import { ReactNative } from '@stone-js/react-native-adapter'

@Routing()
@ReactNative()      // where events come from
@UseReactNative()   // what a resolved route becomes
@StoneApp({ name: 'acme' }, [domainBlueprint])
export class Application {}
import { routerBlueprint } from '@stone-js/router'
import { defineStoneReactNativeApp } from '@stone-js/use-react-native'
import { reactNativeAdapterBlueprint } from '@stone-js/react-native-adapter'

export const Application = defineStoneReactNativeApp(
  { name: 'acme' },
  [routerBlueprint, reactNativeAdapterBlueprint, 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.

app/TaskScreen.tsxtsx
@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.

terminalbash
npm i @stone-js/react-native-adapter @stone-js/use-react-native

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.

terminalbash
npx @stone-js/create@latest acme --starter monorepo-declarative
acme/
├── packages/domain/     @acme/domain   the entities and the behaviour
├── apps/web/            @acme/web      @Browser + @UseReact
└── apps/mobile/         @acme/mobile   @ReactNative + @UseReactNative

Its 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.

metro.config.jsjs
const { getDefaultConfig } = require('expo/metro-config')
const { withStone } = require('@stone-js/use-react-native/metro')

module.exports = withStone(getDefaultConfig(__dirname), __dirname)

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.


Stone.js

Your app exists in every runtime. Until you run it.

An open-source project by Stone Foundation
Created by Mr. Stone (Evens Pierre)