Skip to content

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:

text
Application components
        ↓
Generated @gtkx/jsx elements
        ↓
@gtkx/react renderer
        ↓
Generated @gtkx/gi methods
        ↓
@gtkx/runtime
        ↓
@gtkx/native
        ↓
Native libraries

The renderer translates React commits into native operations. Generated methods supply signatures to the runtime, which converts values and calls the native bridge.

GI bindings also work without React: generated functions and classes call the runtime themselves, and GObject subclasses register through registerClass from @gtkx/runtime (see Subclassing). Component libraries build on JSX and GI APIs and do not depend on how native pointers are represented.

Generation and execution ​

GTKX reads GObject Introspection Repository (GIR) files during code generation, before the application runs.

text
 GIR libraries + element configuration
                   ↓
             @gtkx/codegen
                   ↓
@gtkx/gi + @gtkx/jsx + project reference

During 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 the generated ESM classes and methods. Their descriptors tell @gtkx/runtime and the native bridge how to resolve and call functions in the installed shared libraries through libffi. Native type lookup and GObject property access happen at runtime. GIR analysis belongs to code generation; @gtkx/runtime receives call descriptors from its consumers and does not load libgirepository.

The project's library selection and GIR versions define the generated 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:

  1. The generated JSX component turns its props into a React host element identified by its GType name. Element-valued props become named child slots.
  2. React reconciles the tree and asks GTKX's host configuration to create or update an instance.
  3. The renderer uses generated metadata and registered element behaviors to construct the native object, apply values, and place children through the appropriate container API.
  4. Generated GI methods describe the native calls. Runtime converts their arguments, and the Rust addon prepares native storage and invokes the underlying C functions.
  5. 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.
  6. The handler changes React state. React reconciles again, and the renderer updates the existing native label where the element's identity permits reuse.

React's component tree and the native object tree have different shapes. 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. The generated GIO application override 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 ​

Component, wrapper, and native allocation lifetimes have separate owners:

LifetimeOwnerGTKX's role
Component lifetimeReact's tree, keys, and effects.Mount, update, and unmount host instances; release renderer handlers and behaviors.
JavaScript wrapper lifetimeJavaScript reachability and native wrapper references.Preserve wrapper identity for tracked native objects and associate wrappers with native handles.
Native allocation lifetimeGObject 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. Other JavaScript or native references can keep the object alive. Windows and dialogs also have explicit presentation and close behavior. When investigating a memory bug, check both the React lifetime and the native ownership contract.

A controlled prop and a native widget property represent the same application 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. User input continues through the connected handler.

Finding the responsible layer ​

Observable problemStart 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 packages/native/src/handle.rs.
A JSX prop is missing or has the wrong type.packages/codegen/src/store/jsx and packages/config/src/builtin-elements.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/codegen/overrides/gio/overrides/application.ts, 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.