State management is one of the key architectural decisions in SPAs. When working on complicated frontend views in the framework of Odoo Web Library (OWL), there is a specific challenge that arises for developers - how can we keep the state when there are several views or actions involved? Although the use of state management for each component via OWL's useState hook helps us solve all our problems by keeping the state inside the same component, it does not help us when the component is destroyed (when a user switches from form view to kanban and vice versa).
In this blog, we are going to learn about implementing proper global state management in Odoo by means of Odoo's Service Registry and OWL's reactive utilities.
Core Concept: Singleton Services
A Service in Odoo is a singleton instance that is instantiated at the time of the initialization of the web client. It is meant to provide utility functionalities like orm, rpc, action, and notification to any component by using the useService hook.
When we create our own service and return a reactive OWL object from its start method, then it will act as a reactive global store similar to state management in other frameworks.
Step-by-Step Implementation
Let us create an example: a Global User Preferences Service for handling user interface preferences like hiding the sidebar and using a dark theme through various UI views.
Step 1: Create the Reactive Service
Create a JavaScript file at static/src/js/user_preference_service.js.
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { reactive } from "@odoo/owl";
export const userPreferenceService = {
// Add dependencies if your service needs other services (e.g., 'orm' or 'notification')
dependencies: [],
start(env) {
// Create a reactive state object containing state data and mutation methods
const state = reactive({
sidebarCollapsed: false,
darkMode: false,
toggleSidebar() {
this.sidebarCollapsed = !this.sidebarCollapsed;
},
toggleDarkMode() {
this.darkMode = !this.darkMode;
this._applyTheme();
},
_applyTheme() {
const body = document.body;
if (this.darkMode) {
body.classList.add("dark-theme");
} else {
body.classList.remove("dark-theme");
}
}
});
// Return the reactive state object so components can consume it
return state;
}
};
// Register the service in the main 'services' category
registry.category("services").add("user_preference", userPreferenceService);
Step 2: Register the Assets in the Manifest
Make sure that the Javascript file is included in your module’s manifest __manifest__.py under the appropriate assets bundle (normally web.assets_backend).
{
'name': 'Custom User Preferences',
'version': '1.0',
'depends': ['web'],
'data': [],
'assets': {
'web.assets_backend': [
'my_module/static/src/js/user_preference_service.js',
'my_module/static/src/js/settings_toggle.js',
'my_module/static/src/js/sidebar_component.js',
],
},
}Step 3: Consuming and Mutating State Using OWL Hooks
And now the technical bit: Just putting in the service is not going to give you a reaction to changes. You must use OWL’s useState hook on your service so that it can become dependent on the state.
Component A: Preference Toggler (Navbar Controller)
This component causes mutations to the global preference state.
/** @odoo-module **/
import { Component, useState } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
export class SettingsToggle extends Component {
static template = "my_module.SettingsToggle";
setup() {
// useService provides the singleton instance.
// useState syncs the rendering lifecycle of the component with the state object.
this.preferences = useState(useService("user_preference"));
}
onToggleTheme() {
this.preferences.toggleDarkMode();
}
}
And its XML template (static/src/xml/settings_toggle.xml):
<templates xml:space="preserve">
<t t-name="my_module.SettingsToggle" owl="1">
<button class="btn btn-light" t-on-click="onToggleTheme">
<i t-attf-class="fa #{preferences.darkMode ? 'fa-sun-o' : 'fa-moon-o'}"/>
<span class="ms-2">Toggle Theme</span>
</button>
</t>
</templates>
Component B: The Responsive Sidebar (UI consumer)
This component gets updated and changes its state every time Component A updates the global store.
/** @odoo-module **/
import { Component, useState } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
export class SidebarComponent extends Component {
static template = "my_module.SidebarComponent";
setup() {
// Again, wrapping the service in useState guarantees automatic UI updates.
this.preferences = useState(useService("user_preference"));
}
}
Its XML template (static/src/xml/sidebar_component.xml):
<templates xml:space="preserve">
<t t-name="my_module.SidebarComponent" owl="1">
<div t-attf-class="sidebar #{preferences.sidebarCollapsed ? 'collapsed' : ''} #{preferences.darkMode ? 'dark-sidebar' : ''}">
<div class="sidebar-header">
<h3>Navigation</h3>
</div>
<div class="sidebar-body">
<!-- Navigation Items -->
</div>
</div>
</t>
</templates>
Best Practices & Pitfalls
Use useState always: If you don't use useState to get the reactive service (writing this.preferences = useService("user_preference")), the component will initialize the values, but it will never re-render when there are changes in the service's values.
Stay Focused: Do not create one Global State Service for all your app. Instead, create services that do what they need to do (cartService, userPreferenceService, multiUserSyncService).
Do not modify directly: Do not modify the state properties directly in your components (this.preferences.darkMode = true). Instead, create helper functions inside the service object (this.preferences.toggleDarkMode()).
The integration of the reactive primitives of the OWL language with the Service Registry of Odoo provides an elegant and lightweight approach to managing the global state of an application without having to import large amounts of external libraries.
To read more about How to Create a standalone Owl application in Odoo 19, refer to our blog How to Create a standalone Owl application in Odoo 19.