Enable Dark Mode!
how-to-build-custom-services-with-owl-in-odoo-19.jpg
By: Abhijith CK

How to Build Custom Services with OWL in Odoo 19

Technical Odoo 19 Odoo Community Odoo Enterprises

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.


Frequently Asked Questions

Can I use useState(useService("...")) inside the standard Odoo views (like Kanban or Form)?

Yes. One can modify standard Odoo controller/view and hook one’s own reactive service to keep the state in sync between the standard Odoo views and custom sidebar/navbar.

What is the main difference between Odoo Services and OWL Context?

Within OWL Context (useSubEnv), the state flow is completely hierarchical and propagates only downward from the parent to child components within the component tree. Two components that are not part of the same direct branch of the component tree cannot ever have the same context. But since Odoo Services are singletons that are globally available, they suit sibling components or completely separate UI components.

Does using global services increase memory usage?

As the services are singletons, there will be only one service state object created throughout the lifespan of the web page. However, do not attach any event listener that keeps references to large objects, as it will prevent the unmounting of the components.

If you need any assistance in odoo, we are online, please chat with us.



0
Comments



Leave a comment



WhatsApp