I’ve hit an architectural roadblock while developing a GNOME Extension. I have a deeply nested UI structure. For instance, I need to share or sync state between a component in the 3rd sub-branch of the 1st main branch and another in the 5th sub-branch of the 2nd main branch.
Here are the methods I’ve considered or tried so far:
Prop Drilling (Manual Transfer): Passing data manually through every single branch. However, as the nesting gets deeper, this leads to messy code and a high margin for error.
Global Event Bus: Defining an event bus on a global object. But as you know, using global objects is generally discouraged in GNOME Shell standards and poses risks for memory management.
I couldn’t find any documentation or guide regarding a “State Management” pattern (similar to Redux or Vuex) for GNOME Extensions.
How can I establish a “single source of truth” logic in this complex tree without increasing tight coupling between components? Is there a recommended “best practice” pattern within the GNOME Shell ecosystem for this kind of communication?
I look forward to suggestions from experienced developers. Thanks in advance!
This is always going to be a tradeoff depending on what your app needs to do, and there’s no single best practice to recommend. I have done both of the things you suggested and they worked.
From what you describe about your UI, I would look into Model-View or Model-View-Presenter patterns. There’s a lot of very Enterprise Java-like writing about them on the web, but don’t let that scare you - the way I usually implement it in GJS is, the model is a GObject class with no UI widgets, the view is a GtkWindow subclass, and the presenter can sometimes be as simple as a bunch of signal connections and calls to GObject.bind_property.
Extensions MAY create static data structures and instances of built-in JavaScript objects (such as Regexp() and Map()), but all dynamically stored memory must be cleared or freed in disable() (e.g. Map.prototype.clear()). All GObject classes, such as Gio.Settings and St.Widget are disallowed.
Am I allowed to define a global state management? I’m not sure I understand, because the documentation above states that this is not allowed. For example, can I use a code like the one below? If this is allowed, I can easily manage state in complex UI structures. However, it says that this is not permitted? In addition to this, should this.run_dispose() be used within destroy function?
import GObject from 'gi://GObject';
export const StateManager = GObject.registerClass(
{
Properties: {
'example-property': GObject.ParamSpec.string(
'example-property',
'Example Property',
'A read-write string property',
GObject.ParamFlags.READWRITE,
null
),
},
},
class _StateManager extends GObject.Object {
public static _instance: _StateManager | null = null;
public static getInstance(): _StateManager {
if (!StateManager._instance) {
StateManager._instance = new StateManager();
}
return StateManager._instance;
}
public destroy() {
StateManager._instance = null;
}
}
);
Global state management doesn’t literally have to mean a global variable hanging off of globalThis. It can be an instance of a class, that’s created in enable() and deleted in disable(). You don’t need to use the singleton pattern.
If I keep the object inside the extension for complex UI, I have to pass it to every component manually. If I use a singleton, I can just import it where I need it. Does this singleton approach break the rule mentioned above?