Architecture
GTKX runs React in Node.js and renders through native Adwaita and GTK4 objects. React owns state and reconciliation; native libraries supply application structure, widgets, layout, input, accessibility, and rendering.
The core layers
The package map lists package responsibilities. At runtime, a JSX update follows this path:
Application components
↓
Generated @gtkx/jsx elements
↓
@gtkx/react renderer
↓
Generated @gtkx/gi methods
↓
@gtkx/runtime → @gtkx/native → Native librariesThe renderer translates React commits into native operations. Generated methods supply signatures to runtime, which converts values and calls the native bridge.
GI bindings also work without React: function calls and GObject subclasses can use runtime directly. Component libraries normally use JSX and GI APIs without depending on native pointer representation.
Generation and execution
GTKX separates information obtained from GObject Introspection Repository files, or GIR files, from operations performed while the application runs.
GIR libraries + element configuration
↓
@gtkx/codegen
↓
@gtkx/gi + @gtkx/jsx + project referenceDuring generation, @gtkx/codegen reads the configured GIR libraries and their transitive dependencies. It determines JavaScript names, TypeScript types, callable signatures, transfer rules, and the metadata needed by the renderer. It emits two linked package stores, @gtkx/gi and @gtkx/jsx, plus a project reference when enabled.
During execution, Node imports those generated ESM modules, including their actual class definitions and method implementations. Their wrappers and descriptors drive @gtkx/runtime and the native bridge, which resolve symbols in the installed shared libraries and call them through libffi. Native type lookup and GObject property operations still happen at runtime. @gtkx/runtime has no knowledge of libgirepository: consumers supply all call descriptors and shapes, and GIR analysis belongs to generation.
This distinction explains why generated imports are local project artifacts. The project's library selection and GIR versions define the available API. Changing a GIR search path changes generated declarations; it does not install a native library that implements those declarations. See Configuration and Codegen for the application-facing configuration.
The entry points for this boundary are the codegen runner, the runtime callable adapter, and the native binding API.
A render and an interaction
Consider a component that renders a button and updates a label when the button is clicked. Its path through GTKX is:
- The generated JSX component turns its props into a React host element identified by its GType name. Element-valued props become named child slots.
- React reconciles the tree and asks GTKX's host configuration to create or update an instance.
- The renderer uses generated metadata and registered element behaviors to construct the native object, apply values, and place children through the appropriate container API.
- Generated GI methods describe the native calls. Runtime converts their arguments, and the Rust addon prepares native storage and invokes the underlying C functions.
- GTK delivers the button's signal through the GLib main context. The runtime calls the connected JavaScript handler, and the renderer gives that handler React's discrete event priority.
- The handler changes React state. React reconciles again, and the renderer updates the existing native label where the element's identity permits reuse.
There are two related trees: React's component tree and the native object tree. Function components have no native object of their own. Named slots and parent-created page objects introduce additional routing, and portals can place a native object outside its surrounding React subtree while preserving React context. The renderer page explains those differences.
One owning event-loop thread
The native addon acquires GLib's default main context on the Node thread that initializes it. It integrates that context with libuv using prepare, timer, and file-descriptor polling handles. Node remains the outer event loop, allowing Node I/O, timers, promises, React scheduling, and GLib sources to make progress in the same application process.
Application startup is adapted to that arrangement. GTKX invokes GLib's local command-line handling to register and activate an application without entering a blocking g_application_run() loop. The application keeps the Node loop alive while active, and its teardown releases that hold. React's application component waits for activation before rendering its children.
The owning-thread requirement extends to native wrappers and widget operations. Worker threads are useful for computation with plain data, which can be sent back to the owning thread. They are not additional GTK render threads. Details and source links are in Native Runtime.
State and ownership
Three forms of lifetime meet in an application:
| Lifetime | Owner | GTKX's role |
|---|---|---|
| Component lifetime | React's tree, keys, and effects. | Mount, update, and unmount host instances; release renderer handlers and behaviors. |
| JavaScript wrapper lifetime | JavaScript reachability and native wrapper references. | Preserve wrapper identity for tracked native objects and associate wrappers with native handles. |
| Native allocation lifetime | GObject reference counts or each native type's copy, reference, and free operations. | Honor ownership descriptors and release allocations through the correct mechanism. |
Unmounting a component removes its placement and renderer-managed connections. It does not imply that every JavaScript reference to the object has vanished or that every native reference count has reached zero. Windows and dialogs also have explicit presentation and close behavior. Memory bugs therefore require examining both the React lifetime and the native ownership contract.
Likewise, a controlled prop and a native widget property are two representations of state. Writing a prop can itself emit a native signal. The renderer tracks its own mutations and suppresses configured user-event signals caused by those writes so they do not become feedback loops. Genuine user input continues through the connected handler.
Finding the responsible layer
| Observable problem | Start reading |
|---|---|
| A generated method has an incorrect type, argument order, or return shape. | packages/codegen/src/gir, analysis, and store/gi. |
| A correctly described call produces an invalid value or loses native memory. | packages/runtime/src/fn.ts, then packages/native/src/ffi/codec and handle.rs. |
| A JSX prop is missing or has the wrong type. | packages/codegen/src/store/jsx and packages/react/src/element-config.ts. |
| A prop type is correct but the widget receives the wrong update. | packages/react/src/reconciler/apply-props.ts and the registered element behaviors. |
| A child appears in the wrong place, disappears, or fails to reorder. | packages/react/src/reconciler/child-routing.ts and placement.ts. |
| Signals, timers, or shutdown stop making progress. | packages/runtime/src/lifecycle.ts and packages/native/src/runloop.rs. |
A renderer symptom can begin in generated metadata; a native ownership failure can begin in an incorrect GIR annotation. Follow the failing operation across the relevant boundaries.