Day for React Native developers

React Native and Day both use native platform widgets. Day expresses the UI in Rust and updates widgets through signal bindings rather than React reconciliation. This guide maps components to pieces, hooks to signals, and npm packages to Cargo dependencies, with examples of each workflow.

React Native terms in Day

React NativeDay
function componenta plain function returning a Piece
JSXfunction calls and tuples
useStateSignal::new
useMemoMemo::new (no dependency array)
useEffectEffect::new / bind / watch (no dependency array)
re-render + reconciliationno re-render; one binding patches one widget
Contextwith_environment / environment::<T>()
Redux / Zustand / Jotaisignals in plain Rust (module statics, structs)
<FlatList>list (recycling), each (keyed)
React Navigationnav, nav_stack, and string routes
useWindowDimensions() + your own breakpointssize_class(), and a nav that re-presents itself
package.json + MetroCargo.toml (package) + Day.toml (app manifest)
require('./logo.png')image(res::images::logo), a generated constant
i18next / react-intlFluent .ftl + generated res::str functions
Turbo Modules / native componentsparts and pieces
Fast Refreshnone — day relaunch rebuilds and relaunches
eas build / fastlane / Gradle + Xcodeday pack -p <target>

The same counter in both

React NativeDay
import { useState } from 'react';
import { View, Text, Button } from 'react-native';

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <View style={{ gap: 12, padding: 16 }}>
      <Text>{count} clicks</Text>
      <Button title="+" onPress={() => setCount(c => c + 1)} />
    </View>
  );
}
use day::prelude::*;

fn counter() -> impl Piece {
    let count = Signal::new(0i64);
    column((
        label(move || format!("{} clicks", count.get())),
        button("+").action(move || count.update(|c| *c += 1)),
    ))
    .spacing(12.0)
    .padding(16.0)
}

Both wire the tap the same way and differ in what follows it. React re-runs Counter, builds new element trees, and diffs them against the shadow tree before committing to native views. Day re-runs the one closure inside label and calls one native setter. The function itself never runs again, so memo() and useCallback have no equivalent, and stale closures can’t happen: a closure that reads a signal reads the current value.

Controlled components vs. two-way signals

An RN TextInput is controlled by a value/onChangeText pair you wire by hand. Day’s inputs take the signal itself; the two directions are built in, origin-tagged so there’s no echo.

React NativeDay
const [name, setName] = useState('');
const [volume, setVolume] = useState(40);
const [subscribed, setSubscribed] = useState(false);

<View style={{ gap: 12 }}>
  <TextInput
    value={name}
    onChangeText={setName}
    placeholder="Your name"
  />
  <Slider
    value={volume}
    maximumValue={100}
    onValueChange={setVolume}
  />
  <Switch value={subscribed} onValueChange={setSubscribed} />
</View>
let name = Signal::new(String::new());
let volume = Signal::new(40.0);
let subscribed = Signal::new(false);

column((
    text_field(name).placeholder("Your name"),
    slider(volume).range(0.0..=100.0),
    toggle(subscribed),
))
.spacing(12.0)

(Slider lives in @react-native-community/slider, outside RN core. Day ships sliders, toggles, progress, pickers, and a recycling list as built-ins.)

Lists

FlatList maps to list: both virtualize, and both want a key. Day’s rows recycle native cells (UITableView-style), and the key function is a required argument, where keyExtractor is optional, so reconciliation is always by identity.

React NativeDay
<FlatList
  data={items}
  keyExtractor={item => item.id}
  renderItem={({ item }) => (
    <Text style={styles.row}>{item.title}</Text>
  )}
/>
list(
    move || items.get(),
    |item| item.id,
    |slot: ItemSlot<Todo, u64>| {
        label(move || slot.get().title).padding(8.0)
    },
)

React Navigation’s stack keeps navigation state inside the navigator; you drive it through the imperative navigation prop. In Day the path is your signal, and the native container (UINavigationController, the Android back stack) is reconciled to it. The back gesture and hardware back button write pops into your state.

React NativeDay
const Stack = createNativeStackNavigator();

<NavigationContainer linking={linking}>
  <Stack.Navigator>
    <Stack.Screen name="Home" component={HomeScreen} />
    <Stack.Screen name="Album" component={AlbumScreen} />
  </Stack.Navigator>
</NavigationContainer>;

// inside a screen:
navigation.navigate('Album', { id: '42' });
let path = Signal::new(Vec::<String>::new());

nav_stack(path, home_page())
    .title("Home")
    .destination(|key| album_page(key))

// from anywhere — it's just state:
navigate("album-42");
current_route();          // Some("album-42")
nav_back();

The same route strings serve navigation, the DAY_DEEPLINK launch variable, and dayscript test assertions, so deep links need no separate linking config. See Navigation.

Adaptive layout

React Native gives you useWindowDimensions() and leaves the breakpoints to you (or to a library like react-native-paper’s adaptive components). Day buckets the window into size classes (Android’s numbers, unchanged on every backend), and a nav re-presents itself when the window crosses one, on a tablet, in split-screen, or in a resized desktop window.

React NativeDay
const { width } = useWindowDimensions();
const wide = width >= 840;

return (
  <View style={{ flex: 1, flexDirection: wide ? 'row' : 'column' }}>
    {wide && <Sidebar items={items} selected={page} onSelect={setPage} />}
    <View style={{ flex: 1 }}>{renderPage(page)}</View>
    {!wide && <BottomTabs items={items} selected={page} onSelect={setPage} />}
  </View>
);
// One nav host, both presentations. Day re-resolves on every breakpoint
// crossing and re-homes the pages it already built, so scroll offsets and
// focus survive the morph rather than being rebuilt.
nav(page)
    .style(NavStyle::Sidebar)
    .destination(|key| page_for(key))

// Where the app wants to make the same call itself:
let two_up = size_class().is_some_and(|c| c.width >= WidthClass::Expanded);

The class is per window, so an app showing two windows at different sizes gets a separate answer for each.

Styling and layout

RN styles are flexbox via Yoga. Day also owns its layout engine; it uses a proposal protocol (parent proposes, child chooses), closer to SwiftUI than to CSS:

React NativeDay
const styles = StyleSheet.create({
  card: {
    padding: 16,
    backgroundColor: '#1E293B',
    borderRadius: 12,
    alignItems: 'flex-start',
    gap: 8,
  },
  title: { fontSize: 22, fontWeight: '600' },
});

<View style={styles.card}>
  <Text style={styles.title}>Plan</Text>
  <Text>Pro</Text>
</View>;
column((
    label("Plan").font(Font::Title),
    label("Pro"),
))
.spacing(8.0)
.align(HAlign::Leading)
.padding(16.0)
.background(Color::hex(0x1E293B))
.corner_radius(12.0)

There are two differences. flex: 1 becomes .grow(), and containers don’t stretch children by default: a column is as wide as its widest child. And fonts are semantic-first: Font::Title resolves to each platform’s typography scale, so text sits correctly next to native controls without per-platform size tables. Layout and Styling cover both systems.

State management and async

The hooks rules (call order, dependency arrays, exhaustive-deps lint) exist because React re-runs your function and must reattach state each time. Day’s function runs once, so none of that machinery exists. Signals are created wherever you like, read anywhere, and dependencies are discovered by tracking the reads:

let items = Signal::new(Vec::<Item>::new());
let total = Memo::new(move || items.with(|v| v.iter().map(|i| i.price).sum::<f64>()));

// No dependency array — this label re-runs exactly when `total`'s value changes:
label(move || format!("{:.2} €", total.get()))

For app-wide state, signals in a module or struct are already observable from any Piece, so a store library has no role. Async is async/await, with continuations on the UI thread so completions are plain writes:

React NativeDay
const [status, setStatus] = useState('');

useEffect(() => {
  let cancelled = false;
  (async () => {
    const resp = await fetch(url);
    const text = await resp.text();
    if (!cancelled) setStatus(text);
  })();
  return () => { cancelled = true; };
}, [url]);
use day_part_http::{Request, fetch_future};

let status = Signal::new(String::new());

day::task(async move {
    match fetch_future(Request::get(url)).await {
        Ok(resp) => status.set(resp.text().into_owned()),
        Err(e) => status.set(format!("error: {e}")),
    }
});

The cancelled flag on the left is unnecessary on the right: writes to a signal whose page was disposed are silent no-ops, so a late completion can’t touch a closed page. Cancelling the work itself is explicit: day::task returns a TaskHandle, and handle.abort() drops the future and cancels the in-flight request; nothing auto-cancels on page teardown. HTTP goes through each platform’s networking stack (day-part-http), so system proxies, VPN routing, and certificate stores apply.

Package configuration

React NativeDay
// package.json
{
  "name": "field-notes",
  "version": "1.2.0",
  "dependencies": {
    "react": "19.2.0",
    "react-native": "0.86.0",
    "@react-navigation/native": "^7.0.0",
    "react-i18next": "^15.0.0"
  }
}
// plus: metro.config.js, babel.config.js,
// android/ and ios/ configs for anything native
# Cargo.toml — the package
[package]
name = "field-notes"
version = "1.2.0"
edition = "2024"

[dependencies]
day = { git = "https://github.com/daybrite/day.git" }

# Day.toml — the app manifest
[app]
id = "dev.example.fieldnotes"
title = "Field Notes"
build = 34
targets = ["ios-uikit", "android-mdc", "macos-appkit"]

Cargo resolves and builds everything, including packages with native halves, in one step. A package that needs platform code, the Turbo Module case, is a part (headless capability) or a piece (a widget): it declares its Swift, Java, or C++ in its own crate metadata, and day build folds that into the app’s platform builds. Calls into it are ordinary Rust function calls compiled together with your app, typed end to end.

Resources and localization

Metro resolves require('./logo.png') at bundle time and picks @2x/@3x variants; Day stages resource/images/ into each platform’s native store (asset catalogs, res/drawable-*, GResource), and the platform picks the density. References are generated constants (image(res::images::wave)), so a renamed or deleted file is a compile error, the same guarantee Metro gives at bundle time.

React NativeDay
<Image source={require('./assets/wave.png')} />

// i18next
i18n.use(initReactI18next).init({
  resources: { en: { translation: en }, fr: { translation: fr } },
  lng: 'en',
});
const { t } = useTranslation();
<Text>{t('unread', { count })}</Text>;
image(res::images::wave)   // resource/images/wave.png + @2x/@3x

// resource/locales/en/app.ftl:
//   unread = { $count ->
//       [one] 1 unread
//      *[other] { $count } unread
//   }
res::locales::install();                 // registers every locale directory
label(res::str::unread(count))           // generated, compile-checked

res::str::unread is a function generated from the .ftl catalogs at build time. A missing key or wrong argument count fails the compile instead of rendering a placeholder at runtime. The locale is a signal: switching languages re-renders every string live, and RTL locales mirror the whole layout. See Localization and Resources.

Project structure and per-platform configuration

React NativeDay
FieldNotes/
├── package.json
├── metro.config.js
├── babel.config.js
├── App.tsx
├── src/
├── android/          # Gradle project (yours to maintain)
├── ios/              # Xcode project + Podfile
└── node_modules/
field-notes/
├── Cargo.toml
├── Day.toml
├── src/
│   ├── lib.rs        # the app: pieces, signals, routes
│   └── main.rs       # desktop entry point
├── resource/         # assets/ images/ fonts/ icons/ locales/
├── dayscript/        # UI test flows
├── platform/
│   ├── ios/          # Xcode host (no app logic)
│   ├── android/      # Gradle host (no app logic)
│   └── ohos/         # HarmonyOS host
└── build/day/        # everything generated

Both projects carry android/ and ios/ directories. Day’s hosts are host projects that load the Rust library, and app-level configuration (id, title, version, window, permissions, signing) lives in Day.toml, with per-platform overrides:

[app]
id = "dev.example.fieldnotes"
title = "Field Notes"

[app.ios]
title = "Field Notes Mobile"        # only iOS sees this

[app.macos-appkit]
id = "dev.example.fieldnotes.mac"   # only the macOS target sees this

That’s the role app.json plays in Expo, extended to all eight primary targets, desktop included. The hosts are already in your repository, so there is nothing to eject.

Building and shipping

TaskReact NativeDay
run in devnpx react-native run-ios / run-androidday launch -p ios-uikit / -p android-mdc
apply changesFast Refreshday relaunch --all-running
Android store buildGradle bundleRelease + signing configday pack -p android-mdc → signed .apk + .aab
iOS store buildXcode archive / fastlane / EASday pack -p ios-uikit → .ipa via xcodebuild -exportArchive
macOS / Windows / Linuxreact-native-macos / react-native-windows (separate forks)day pack -p macos-appkit / -p windows-xaml / -p linux-gtk — .dmg (notarized), .msix + installer, .flatpak + .appimage
webreact-native-web (renders DOM via the RN API)day build -p web-dom — the same crate compiled to WebAssembly, driving DOM elements
environment checknpx react-native doctorday doctor

Maturity differs by target, and Platform support lists what each target has today: every (OS, toolkit) pair carries a support tier, from Tier 1 (thoroughly tested, with shipping apps on it) down to Tier 4 (development combinations nobody ships).

You give up Fast Refresh and the size of the npm ecosystem, and you write Rust instead of TypeScript; in return one repository and one dayscript cover mobile, desktop, and web. Signing secrets are ${ENV_VAR} references in Day.toml; a missing one degrades to a dev-signed build with a warning rather than failing. Packaging has the full table.

Where to go next

  • Getting started — install, scaffold, first launch.
  • API tour — examples of common UI components and patterns.
  • Reactivity — why there are no dependency arrays.
  • Why Day compares the approaches and says when React Native fits better.