Toolchains: Metro or vite
react-native-gtkx supports two toolchains, and neither is the "real" one with the other bolted on — both consume the same published package and render through the same bridge. Which one a project uses is a question about where the project comes from, not a tier of support:
- Adding Linux to an existing React Native app (already has
ios/androidand a Metro config): stay on Metro.npx react-native run-linux --devgives Fast Refresh,run-linux/build-linuxare the same commands the other platforms use, just for--platform linux. - Starting a Linux-first project: the template, on the vite preset.
gtkx devgives Fast Refresh, builds are single-file bundles.
See Installation for setting up either one and Packaging for what each produces when it's time to ship.
The vite path
gtkx dev and gtkx build are the two commands a vite-path project
runs day to day; both start vite themselves and pick up the project's
vite.config.ts (with the react-native-gtkx/vite preset applied)
automatically.
gtkx dev [entry] # dev server + Fast Refresh; entry defaults to
# src/index{.tsx,.jsx,.ts,.js}
gtkx build [entry] # production bundle
Both accept --cwd=<path> to run against a project root other than the
current directory. gtkx build also accepts --asset-base=<path>, an
asset base path relative to the executable's directory, for layouts
where the bundle and its assets don't sit side by side. gtkx codegen
(with --force to wipe and regenerate a corrupted store) generates the
@gtkx/gi/@gtkx/jsx bindings for the GIR libraries declared in
gtkx.config.ts — gtkx dev/gtkx build run it automatically; a bare
vitest run does not (see Testing below).
The Metro path
run-linux, build-linux and deploy-linux are contributed to the React
Native CLI by react-native-gtkx's own react-native.config.js — no
separate install, they come with the package.
npx react-native run-linux [--entry-file <path>] [--bundle-output <path>]
[--skip-bundling] [--dev] [--port <number>]
npx react-native build-linux [--entry-file <path>] [--bundle-output <path>]
[--standalone] [--sea] [--sea-output <path>]
npx react-native deploy-linux [--entry-file <path>] [--target <formats>]
[--out <path>] [--print-manifests] [--skip-build]
run-linux bundles with Metro and opens the window; --dev starts (or
reuses) the Metro dev server on --port (default 8081) instead, for
Fast Refresh. build-linux bundles for distribution and stops short of
running it — see Packaging for --standalone/--sea,
the two flags that turn the Metro bundle into something shippable.
deploy-linux goes one step further and builds an installable .deb/
.rpm/.AppImage from that same Metro build — also covered on the
Packaging page, which is where its vite-path counterpart
lives too (the same command name works for either toolchain).
The React Compiler (vite path only)
gtkx dev and gtkx build run the React
Compiler over every source file
in the project — never node_modules. It is on unless gtkx.config.ts
turns it off:
export default defineConfig({
libraries: ["Gtk-4.0", "Adw-1"],
applicationId: "com.example.myapp",
reactCompiler: false,
})
Omitting the option and setting it to true mean the same thing — only
an explicit false disables it. The Metro path (run-linux/
build-linux) keeps the app's stock Babel preset and never runs the
compiler.
If a ported app misbehaves in a way that smells like stale rendering —
a value that should have updated didn't — set reactCompiler: false and
see whether the symptom goes away. That one line tells you which half of
the system to debug: if the symptom disappears, the underlying issue is
a Rules of React violation the
compiler is compiling around correctly, and the real fix is moving the
offending read into state, a ref, or a hook — not leaving the compiler
off. A typical shape: a component reading mutable module-level state
during render has that read memoized by the compiler, so it renders
fourteen times and shows the same value every time — it looks like a
broken counter and is in fact a working one behind a cached render.
On React Native the compiler is opt-in, so an app that doesn't follow the Rules of React still works there. Here it's on by default, so the same violations become visible misbehavior on a platform where everything else is also new — which reads as a platform bug when it's a Rules-of- React one.
Reanimated shared values have their own spelling for the same reason:
sharedValue.value = x and sharedValue.set(x) both work, but only
.get()/.set() passes compiler-aware lint (react-hooks/immutability
treats anything a hook returns as frozen). Prefer .get()/.set() in
new code; .value keeps working, so a ported app doesn't need a
rewrite.
Bundler aliases
Both presets rewrite the same six package names: react-native itself,
plus react-native-svg, react-native-reanimated,
react-native-worklets, react-native-gesture-handler and
react-native-reanimated-dnd onto their compat subpaths. The
react-native-svg alias is the one most projects notice at the type
level: the alias is a bundler-time rewrite, so TypeScript still needs
its own answer for the bare "react-native-svg" specifier — an
unresolved import in the editor even though the build works. Which fix
applies depends on the project:
- Also ships to iOS/Android/web: install the real
react-native-svg— the app needs it there regardless, and it ships its own.d.ts. The Linux build never executes that package's code (the alias rewrites the import before Node sees it). - Linux-only project (the template, or an app with no mobile
target): install
react-native-svgas a devDependency purely for its types —npm install -D react-native-svg. The ordinary fix for a bundler-alias setup with no real package installed.
Testing
react-native-gtkx ships its GTK component-testing recipe as two subpaths, so a consumer app doesn't have to rediscover it:
react-native-gtkx/vitest—reactNativeGtkxTest(), a ready Vitest project config: the headless-compositor plugin, thereact-nativealias and platform extensions, and the React act-environment setup;react-native-gtkx/testing— re-exports@gtkx/testing's render/screen/userEvent/fireEvent surface, plusrenderHookWithWindowfor hooks that read the active window (useWindowDimensionsand similar).
import { reactNativeGtkxTest } from "react-native-gtkx/vitest"
import { defineConfig } from "vitest/config"
export default defineConfig(reactNativeGtkxTest())
The default test glob is **/*.gtk.test.{ts,tsx}. Component tests need a
headless Wayland compositor and D-Bus on PATH — sway xwayland dbus on
Ubuntu — and gtkx codegen must already have generated the project's
@gtkx/gi bindings, since a bare vitest run doesn't trigger codegen
itself the way gtkx dev/gtkx build do; wire that as a pretest
script. A missing compositor fails a test run with a readable error
rather than hanging.
import { Root } from "react-native"
import { render, screen } from "react-native-gtkx/testing"
// react-native-gtkx components need a layout root — AppRegistry.runApplication()
// in the real app, <Root> in a test.
await render(
<Root
width={800}
height={600}
>
<App />
</Root>,
)
expect(screen.getByText("Hello, GNOME!")).toBeTruthy()
MCP server for agents
An agent working inside a project that depends on react-native-gtkx can
ask the library about itself instead of guessing:
react-native-gtkx-mcp is a Model Context
Protocol server shipped as a bin on
the package. Register it project-level (Claude Code's .mcp.json, or
the equivalent config of any MCP-compatible client):
{
"mcpServers": {
"react-native-gtkx": { "command": "npx", "args": ["react-native-gtkx-mcp"] }
}
}
npx react-native-gtkx-mcp from the project root resolves the locally
installed version, so it always answers for the exact react-native-gtkx
version the project has. Three tools:
rn_gtkx_list_surface— browse the surface without knowing a name first (portable components/APIs, gtk/adw widgets, common), with counts;rn_gtkx_describe_component— the one to reach for first: does a component/widget exist, which subpath it's exported from, what GTK widget backs it, what differs from React Native, whether it takesstyle/onLayoutor is raw;rn_gtkx_search_docs— free-text fallback for symptoms and known-issue questions the other two can't answer by name.
It runs without GTK installed — no @gtkx/* import anywhere in it,
reading only the package's own bundled docs/manifest data. That matters
in practice: an agent is often reading the project from a machine with
no GTK toolchain at all.