Skip to Content
  • Home
  • Book
  • Blog
  • About us
  • Help
  • Contact us
  •  [email protected]
  • Sign in
  • English (US) Español (BO)
  • Simplify It S.R.L.
    • Contact Us
Simplify It S.R.L.
      • Home
      • Book
      • Blog
      • About us
      • Help
      • Contact us
    •  [email protected]
    • English (US) Español (BO)
    • Sign in
    • Contact Us

    Chapter 11: Modern Hooks - useEffect and useRef

  • All Blogs
  • OWL Book
  • Chapter 11: Modern Hooks - useEffect and useRef
  • August 25, 2026 by
    Chapter 11: Modern Hooks - useEffect and useRef
    Grover Menacho

    Why this chapter? Lifecycle hooks alone push you toward writing code organized by "when," not "why" — a timer's setup ends up in onMounted, its cleanup in onWillUnmount, and the two drift apart as the component grows. Modern hooks let you keep everything about one concern together. What is Odoo trying to solve with this? Odoo's own backend components juggle a lot of external resources — DOM measurements, subscriptions, timers, focus management — and useEffect/useRef give a single, predictable place to set them up and tear them down without scattering that logic across multiple lifecycle methods. Real-world application: Debounced search boxes, auto-focus on a newly opened dialog, and syncing a chart library to component state are all things I've shipped in client addons using exactly the patterns in this chapter.

    In Chapter 10, we explored the lifecycle hooks like onMounted, onWillStart, and onWillUnmount. These hooks are powerful and intuitive, but modern OWL development encourages a more functional and flexible approach using modern hooks, primarily useEffect and useRef.

    Modern hooks represent a paradigm shift in how we think about component logic. Instead of organizing code by lifecycle events ("when the component mounts, do this"), we organize it by features or concerns ("everything related to this timer lives together"). This approach makes code more maintainable, testable, and easier to reason about.

    Let's dive deep into these modern hooks and learn how they can make your components more powerful and elegant.


    useRef: Beyond DOM References

    We briefly encountered useRef in Chapter 10 for accessing DOM elements. While DOM access is its most common use case, useRef is actually a much more versatile hook for creating stable references to any value.

    Understanding Refs

    In OWL, a ref is an object with a single .el property that points to the DOM element marked with the matching t-ref directive in your template. The key characteristic of a ref is that it's stable across re-renders—it always points to the same object instance, and .el is kept up to date as the DOM changes.

    Coming from React? OWL has no "value refs" with a .current property. It doesn't need them: OWL components are class instances, so for mutable values that shouldn't trigger re-renders (timer IDs, previous values, library instances) you simply use plain instance properties like this.timer. useRef in OWL is exclusively for DOM access.

    DOM References: The Classic Use Case

    Let's start with the familiar DOM reference pattern:

    JavaScript (focus_manager.js):

    import { Component, useRef, useState } from "@odoo/owl";
    
    export class FocusManager extends Component {
      static template = "my_module.FocusManager";
    
      setup() {
        this.state = useState({ 
          inputValue: "",
          focusCount: 0 
        });
    
        // Create refs for multiple elements
        this.nameInputRef = useRef("name-input");
        this.emailInputRef = useRef("email-input");
        this.submitButtonRef = useRef("submit-button");
      }
    
      focusName() {
        if (this.nameInputRef.el) {
          this.nameInputRef.el.focus();
          this.state.focusCount++;
          console.log("FocusManager: Focused name input");
        }
      }
    
      focusEmail() {
        if (this.emailInputRef.el) {
          this.emailInputRef.el.focus();
          this.state.focusCount++;
          console.log("FocusManager: Focused email input");
        }
      }
    
      focusSubmit() {
        if (this.submitButtonRef.el) {
          this.submitButtonRef.el.focus();
          this.state.focusCount++;
          console.log("FocusManager: Focused submit button");
        }
      }
    
      handleKeyDown(event) {
        // Navigate between fields with Tab
        if (event.key === "Tab" && event.shiftKey) {
          // Handle custom tab navigation if needed
          console.log("FocusManager: Shift+Tab pressed");
        }
      }
    
      onInputChange(event) {
        this.state.inputValue = event.target.value;
    
        // Auto-focus next field when current is filled
        if (event.target === this.nameInputRef.el && event.target.value.length > 2) {
          setTimeout(() => this.focusEmail(), 100);
        }
      }
    }
    

    Template (focus_manager.xml):

    <?xml version="1.0" encoding="UTF-8"?>
    <templates xml:space="preserve">
        <t t-name="my_module.FocusManager" owl="1">
            <div class="focus-manager card">
                <div class="card-header">
                    <h5>Focus Manager Demo</h5>
                    <small class="text-muted">Focus events: <t t-esc="state.focusCount"/></small>
                </div>
    
                <div class="card-body">
                    <form t-on-keydown="handleKeyDown">
                        <div class="mb-3">
                            <label for="name" class="form-label">Name</label>
                            <input 
                                type="text" 
                                class="form-control" 
                                t-ref="name-input"
                                placeholder="Enter your name"
                                t-model="state.inputValue"
                                t-on-input="onInputChange"
                            />
                        </div>
    
                        <div class="mb-3">
                            <label for="email" class="form-label">Email</label>
                            <input 
                                type="email" 
                                class="form-control" 
                                t-ref="email-input"
                                placeholder="Enter your email"
                            />
                        </div>
    
                        <button 
                            type="submit" 
                            class="btn btn-primary me-2"
                            t-ref="submit-button">
                            Submit
                        </button>
                    </form>
    
                    <div class="mt-3">
                        <small class="text-muted">Quick Focus:</small><br/>
                        <button class="btn btn-sm btn-outline-secondary me-1" t-on-click="focusName">
                            Focus Name
                        </button>
                        <button class="btn btn-sm btn-outline-secondary me-1" t-on-click="focusEmail">
                            Focus Email
                        </button>
                        <button class="btn btn-sm btn-outline-secondary" t-on-click="focusSubmit">
                            Focus Submit
                        </button>
                    </div>
                </div>
            </div>
        </t>
    </templates>
    

    Storing Mutable Data: Plain Instance Properties

    What about mutable values that should not trigger re-renders when changed—timer IDs, previous values for comparison, third-party library instances? In OWL you don't need a hook for these at all. Because your component is a class instance, a plain property does the job:

    Example: Timer with Instance Properties

    import { Component, useState } from "@odoo/owl";
    
    export class TimerComponent extends Component {
      static template = "my_module.TimerComponent";
    
      setup() {
        this.state = useState({ 
          seconds: 0,
          isRunning: false 
        });
    
        // Plain instance properties - changing them never causes a re-render
        this.timer = null;            // timer ID
        this.previousSeconds = 0;     // previous value for comparison
        this.startCount = 0;          // how many times the timer was started
      }
    
      startTimer() {
        if (this.state.isRunning) return;
    
        console.log("TimerComponent: Starting timer");
        this.state.isRunning = true;
        this.startCount += 1;
    
        // Store timer ID on the instance - this won't trigger re-render
        this.timer = setInterval(() => {
          this.state.seconds++;
    
          // Log every 10 seconds using previous value comparison
          if (this.state.seconds % 10 === 0 && this.state.seconds !== this.previousSeconds) {
            console.log(`TimerComponent: ${this.state.seconds} seconds elapsed`);
            this.previousSeconds = this.state.seconds;
          }
        }, 1000);
      }
    
      stopTimer() {
        if (!this.state.isRunning) return;
    
        console.log("TimerComponent: Stopping timer");
        this.state.isRunning = false;
    
        // Clear timer using stored ID
        if (this.timer) {
          clearInterval(this.timer);
          this.timer = null;
        }
      }
    
      resetTimer() {
        this.stopTimer();
        this.state.seconds = 0;
        this.previousSeconds = 0;
        console.log("TimerComponent: Timer reset");
      }
    
      get timerStats() {
        return {
          currentTime: this.formatTime(this.state.seconds),
          startCount: this.startCount,
          isActive: this.timer !== null
        };
      }
    
      formatTime(seconds) {
        const mins = Math.floor(seconds / 60);
        const secs = seconds % 60;
        return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
      }
    }
    

    The rule of thumb: reactive data that the template shows goes in useState; everything else is a plain instance property; useRef is only for DOM elements.


    useEffect: The Swiss Army Knife of Side Effects

    The useEffect hook is the most powerful and flexible hook in modern OWL. It replaces multiple lifecycle hooks with a single, unified API that groups related logic together.

    What's a "side effect"? It's anything a piece of code does that reaches outside of computing its return value — starting a timer, adding an event listener, calling the server, changing document.title. Rendering should be a pure calculation of "state in, DOM out"; useEffect is where you put the parts that aren't.

    Understanding Dependencies

    The magic of useEffect lies in its second parameter: a dependency function that returns the list of values the effect depends on. OWL calls this function on every render and re-runs the effect only when one of the returned values changed.

    Careful if you know React: in React the dependencies are a plain array ([state.count]). In OWL they are a function that returns an array (() => [state.count]). Passing a plain array is a common migration bug.

    // Runs once after mount, cleanup on unmount (like onMounted + onWillUnmount)
    useEffect(
      () => {
        console.log("Component mounted");
      },
      () => []
    );
    
    // Runs after every render (usually not what you want) - omit the dependency function
    useEffect(() => {
      console.log("Component rendered");
    });
    
    // Runs when specific values change
    useEffect(
      () => {
        console.log("Count or name changed");
      },
      () => [this.state.count, this.state.name]
    );
    

    Pattern 1: Mount-Only Effects (Replacing onMounted)

    Let's create a component that sets up multiple things when it mounts:

    JavaScript (dashboard.js):

    import { Component, useEffect, useRef, useState } from "@odoo/owl";
    import { useService } from "@web/core/utils/hooks";
    
    export class Dashboard extends Component {
      static template = "my_module.Dashboard";
    
      setup() {
        this.state = useState({
          currentTime: new Date().toLocaleTimeString(),
          stats: {
            users: 0,
            orders: 0,
            revenue: 0
          },
          loading: true
        });
    
        this.orm = useService("orm");
        this.titleRef = useRef("page-title");
    
        // Mount-only effect - runs once after component mounts
        useEffect(() => {
          console.log("Dashboard: Component mounted, initializing...");
    
          // 1. Set up clock
          const clockInterval = setInterval(() => {
            this.state.currentTime = new Date().toLocaleTimeString();
          }, 1000);
    
          // 2. Focus the title
          if (this.titleRef.el) {
            this.titleRef.el.focus();
          }
    
          // 3. Load initial data
          this.loadDashboardData();
    
          // 4. Set up window focus listener
          const handleWindowFocus = () => {
            console.log("Dashboard: Window focused, refreshing data");
            this.loadDashboardData();
          };
    
          window.addEventListener('focus', handleWindowFocus);
    
          // Cleanup function - runs when component unmounts
          return () => {
            console.log("Dashboard: Cleaning up mount effects");
            clearInterval(clockInterval);
            window.removeEventListener('focus', handleWindowFocus);
          };
        }, () => []); // Empty dependency list = run once on mount
      }
    
      async loadDashboardData() {
        try {
          console.log("Dashboard: Loading dashboard statistics");
    
          // Simulate loading multiple stats in parallel
          const [users, orders] = await Promise.all([
            this.orm.searchCount("res.users", []),
            this.orm.searchCount("sale.order", [["state", "=", "sale"]])
          ]);
    
          this.state.stats = {
            users,
            orders,
            revenue: orders * 150 // Simulated revenue calculation
          };
    
          this.state.loading = false;
          console.log("Dashboard: Statistics loaded successfully");
    
        } catch (error) {
          console.error("Dashboard: Failed to load statistics:", error);
          this.state.loading = false;
        }
      }
    }
    

    Pattern 2: Reactive Effects (Replacing onWillUpdateProps)

    Effects can react to specific state or prop changes:

    JavaScript (user_profile.js):

    import { Component, useEffect, useState } from "@odoo/owl";
    import { useService } from "@web/core/utils/hooks";
    
    export class UserProfile extends Component {
      static template = "my_module.UserProfile";
      static props = { 
        userId: { type: Number } 
      };
    
      setup() {
        this.state = useState({
          user: null,
          loading: false,
          error: null,
          loadCount: 0
        });
    
        this.orm = useService("orm");
    
        // Effect that reacts to userId prop changes
        useEffect(() => {
          if (!this.props.userId) {
            console.log("UserProfile: No user ID provided");
            this.state.user = null;
            return;
          }
    
          console.log(`UserProfile: Loading user data for ID ${this.props.userId}`);
          this.loadUserData(this.props.userId);
    
        }, () => [this.props.userId]); // Runs when userId changes
    
        // Separate effect for logging load attempts
        useEffect(() => {
          if (this.state.loadCount > 0) {
            console.log(`UserProfile: Load attempt #${this.state.loadCount}`);
          }
        }, () => [this.state.loadCount]);
      }
    
      async loadUserData(userId) {
        this.state.loading = true;
        this.state.error = null;
        this.state.loadCount += 1;
    
        try {
          const users = await this.orm.read("res.users", [userId], [
            "name", 
            "email", 
            "phone", 
            "partner_id",
            "login_date"
          ]);
    
          if (users.length > 0) {
            this.state.user = users[0];
            console.log(`UserProfile: Successfully loaded ${users[0].name}`);
          } else {
            throw new Error(`User ${userId} not found`);
          }
    
        } catch (error) {
          console.error(`UserProfile: Failed to load user ${userId}:`, error);
          this.state.error = error.message;
          this.state.user = null;
        } finally {
          this.state.loading = false;
        }
      }
    }
    

    Pattern 3: Multiple Independent Effects

    One of the biggest advantages of useEffect is that you can have multiple effects, each handling a specific concern:

    JavaScript (notification_center.js):

    First, the component sets up its state and its first effect, which only cares about whether the browser is online:

    import { Component, useEffect, useState } from "@odoo/owl";
    import { useService } from "@web/core/utils/hooks";
    
    export class NotificationCenter extends Component {
      static template = "my_module.NotificationCenter";
    
      setup() {
        this.state = useState({
          notifications: [],
          connectionStatus: 'connecting',
          lastUpdate: null,
          unreadCount: 0
        });
    
        this.orm = useService("orm");
    
        // Effect 1: Connection management
        useEffect(() => {
          console.log("NotificationCenter: Setting up connection monitoring");
    
          const checkConnection = () => {
            this.state.connectionStatus = navigator.onLine ? 'connected' : 'disconnected';
          };
    
          // Initial check
          checkConnection();
    
          // Set up listeners
          window.addEventListener('online', checkConnection);
          window.addEventListener('offline', checkConnection);
    
          return () => {
            window.removeEventListener('online', checkConnection);
            window.removeEventListener('offline', checkConnection);
          };
        }, () => []);
    

    Next, a second effect watches connectionStatus and only starts polling for new notifications once the browser is actually online — notice its dependency function returns [this.state.connectionStatus], so it re-runs whenever that value flips:

        // Effect 2: Periodic notification refresh
        useEffect(() => {
          if (this.state.connectionStatus !== 'connected') {
            console.log("NotificationCenter: Skipping refresh - not connected");
            return;
          }
    
          console.log("NotificationCenter: Setting up periodic refresh");
    
          const refreshInterval = setInterval(() => {
            this.refreshNotifications();
          }, 30000); // Refresh every 30 seconds
    
          // Initial load
          this.refreshNotifications();
    
          return () => {
            clearInterval(refreshInterval);
          };
        }, () => [this.state.connectionStatus]); // Re-run when connection status changes
    

    Finally, a third, completely independent effect recalculates the unread count and keeps the browser tab title in sync whenever the notification list changes — it doesn't know or care about the connection logic above:

        // Effect 3: Unread count calculation
        useEffect(() => {
          const unread = this.state.notifications.filter(n => !n.is_read).length;
          this.state.unreadCount = unread;
    
          console.log(`NotificationCenter: ${unread} unread notifications`);
    
          // Update browser title if there are unread notifications
          if (unread > 0) {
            document.title = `(${unread}) Odoo - Notifications`;
          } else {
            document.title = "Odoo";
          }
    
          return () => {
            // Cleanup title on unmount
            document.title = "Odoo";
          };
        }, () => [this.state.notifications.length]);
      }
    
      async refreshNotifications() {
        try {
          console.log("NotificationCenter: Refreshing notifications");
    
          const notifications = await this.orm.searchRead(
            "mail.notification",
            [["res_partner_id", "=", this.currentUserId]],
            ["id", "mail_message_id", "is_read", "create_date"],
            { 
              order: "create_date desc", 
              limit: 50 
            }
          );
    
          this.state.notifications = notifications;
          this.state.lastUpdate = new Date();
    
        } catch (error) {
          console.error("NotificationCenter: Failed to refresh notifications:", error);
        }
      }
    
      get currentUserId() {
        // This would come from a user service in a real app
        return 1; // Simplified for example
      }
    }
    

    Three effects, three concerns, zero tangled logic — that's the payoff of organizing by feature instead of by lifecycle moment.

    Advanced useEffect Patterns

    Pattern: Effect with Async Logic

    useEffect(() => {
      let cancelled = false;
    
      const fetchData = async () => {
        try {
          const result = await this.orm.searchRead("some.model", [], []);
    
          // Only update state if effect hasn't been cancelled
          if (!cancelled) {
            this.state.data = result;
          }
        } catch (error) {
          if (!cancelled) {
            console.error("Fetch failed:", error);
          }
        }
      };
    
      fetchData();
    
      // Cleanup: mark as cancelled to prevent state updates
      return () => {
        cancelled = true;
      };
    }, () => [this.state.someDependency]);
    

    Pattern: Debounced Effect

    Debouncing means delaying a piece of work until a burst of triggering changes has paused for a set amount of time — instead of searching on every keystroke, you wait until the user stops typing for 300ms.

    useEffect(() => {
      const timeoutId = setTimeout(() => {
        // Perform expensive operation only after value has been stable for 300ms
        this.performSearch(this.state.searchTerm);
      }, 300);
    
      return () => {
        clearTimeout(timeoutId);
      };
    }, () => [this.state.searchTerm]);
    

    Combining useRef and useEffect: Powerful Patterns

    When you combine useRef and useEffect, you unlock some very powerful patterns:

    Pattern: Previous Value Comparison

    setup() {
      this.state = useState({ count: 0 });
      this.previousCount = undefined; // plain instance property
    
      useEffect(
        () => {
          console.log(`Count changed from ${this.previousCount} to ${this.state.count}`);
    
          // Store current value for next comparison
          this.previousCount = this.state.count;
        },
        () => [this.state.count]
      );
    }
    

    Pattern: Third-Party Library Integration

    setup() {
      this.chartCanvasRef = useRef("chart-canvas"); // DOM ref (t-ref="chart-canvas")
      this.chart = null;                            // plain property for the library instance
      this.state = useState({ data: [] });
    
      // Set up chart on mount
      useEffect(
        () => {
          if (!this.chartCanvasRef.el) return;
    
          const ctx = this.chartCanvasRef.el.getContext('2d');
          this.chart = new Chart(ctx, {
            type: 'line',
            data: { datasets: [] }
          });
    
          return () => {
            if (this.chart) {
              this.chart.destroy();
            }
          };
        },
        () => []
      );
    
      // Update chart when data changes
      useEffect(
        () => {
          if (this.chart) {
            this.chart.data = this.processChartData();
            this.chart.update();
          }
        },
        () => [this.state.data.length]
      );
    }
    

    Migration Guide: From Lifecycle to Modern Hooks

    If you have existing components using lifecycle hooks, here's how to migrate them:

    Before: Lifecycle Hooks

    setup() {
      this.state = useState({ data: null });
      this.orm = useService("orm");
    
      onWillStart(async () => {
        this.state.data = await this.orm.searchRead("model", [], []);
      });
    
      onMounted(() => {
        document.title = "My Component";
        this.timer = setInterval(this.refresh, 5000);
      });
    
      onWillUnmount(() => {
        document.title = "Odoo";
        if (this.timer) {
          clearInterval(this.timer);
        }
      });
    }
    

    After: Modern Hooks

    setup() {
      this.state = useState({ data: null });
      this.orm = useService("orm");
    
      // Keep onWillStart for pre-render data: useEffect runs AFTER mounting,
      // so replacing it with an effect would bring back the empty-then-flicker render
      onWillStart(async () => {
        this.state.data = await this.orm.searchRead("model", [], []);
      });
    
      // Mount effects (replaces onMounted + onWillUnmount)
      useEffect(
        () => {
          document.title = "My Component";
          const timer = setInterval(this.refresh, 5000);
    
          return () => {
            document.title = "Odoo";
            clearInterval(timer);
          };
        },
        () => []
      );
    }
    

    Note that onWillStart stays: it's the only hook that can delay the first render, so it has no useEffect equivalent.


    Environment Hooks: useEnv, useSubEnv, useChildSubEnv

    Props are great for passing data one level down, from a parent to its direct child. But what about data that dozens of components scattered across the tree all need—the current user, a translation function, feature flags? Passing it down as a prop through every intermediate component (prop drilling, the same problem that motivates the global state store in Chapter 15) gets painful fast.

    OWL's answer is the environment (env): a plain object shared by every component in the tree, set once when the application is mounted and available everywhere without threading it through props.

    useEnv() reads the current component's environment:

    import { Component, useEnv } from "@odoo/owl";
    
    class UserBadge extends Component {
        static template = xml`<span t-esc="env.currentUser.name"/>`;
    
        setup() {
            this.env = useEnv();
        }
    }
    

    Sometimes a component wants to add or override something in the environment for its own subtree—a theme, a scoped configuration—without changing it for the whole app. Two hooks do this, and the difference between them matters:

    • useSubEnv(newValues) — merges newValues into the environment for this component and all of its children.
    • useChildSubEnv(newValues) — merges newValues into the environment for only the children, leaving this component's own env untouched.
    setup() {
        // Every component below this one (children, grandchildren, ...) now
        // sees env.theme === "dark". This component itself does too, because
        // useSubEnv affects the caller as well.
        useSubEnv({ theme: "dark" });
    }
    

    Use useChildSubEnv when a component wants to configure its descendants without implying that the same configuration applies to itself—for example, a <Section title="..."> component that sets a heading level for its children without becoming a heading itself.

    useExternalListener: Listening Outside the Component

    Chapter 1 attached a click listener by hand with addEventListener, and had to remember to call removeEventListener in onWillUnmount to avoid a leak. useExternalListener does both steps for you in one call, which is exactly what you want for listening on window or document—targets outside the component's own DOM that OWL doesn't manage.

    import { Component, useExternalListener } from "@odoo/owl";
    
    class DropdownMenu extends Component {
        setup() {
            // Automatically added on mount and removed on unmount—no
            // onMounted/onWillUnmount pair to write or forget.
            useExternalListener(window, "click", this.closeIfOutside.bind(this));
        }
    
        closeIfOutside(ev) {
            if (!this.el.contains(ev.target)) {
                this.state.open = false;
            }
        }
    }
    

    It takes the target (window, document, or any DOM node), the event name, and a handler, and forwards any extra arguments to addEventListener—so useExternalListener(window, "keydown", this.onKeyDown, { capture: true }) works too.

    One more hook worth knowing about, even though you'll rarely call it directly: useComponent() returns the current component instance. It exists mainly as a building block for writing your own custom hooks that need to reach the calling component—application code almost never needs it.


    Best Practices and Common Pitfalls

    Do's

    1. Group related logic together - Put setup and cleanup for the same feature in one effect
    2. Use multiple effects - Separate concerns into different effects
    3. Include all dependencies - The dependency function should return every reactive value the effect relies on
    4. Return cleanup functions - Prevent memory leaks by cleaning up resources
    5. Use the right storage - Reactive template data in useState, mutable non-reactive values (timer IDs, previous values, library instances) as plain instance properties, useRef only for DOM elements

    Don'ts

    1. Don't pass a plain array as dependencies - OWL expects a function returning an array (() => [..]), not the array itself
    2. Don't use effects for everything - Some logic belongs in event handlers, not effects
    3. Don't replace onWillStart with an effect - Effects run after mounting; only onWillStart can delay the first render
    4. Don't over-complicate - Sometimes lifecycle hooks are clearer for simple cases

    Common Pitfall: Array Instead of Function

    // BAD: React-style plain array - OWL will not track these dependencies
    useEffect(() => {
      this.syncWithServer(this.state.filter);
    }, [this.state.filter]);
    
    // GOOD: a function that returns the dependency array
    useEffect(
      () => {
        this.syncWithServer(this.state.filter);
      },
      () => [this.state.filter]
    );
    

    Why a function? OWL calls it on every render to get fresh values and compare them with the previous ones. A plain array would be evaluated only once, when setup() ran.

    Common Pitfall: Expecting useSubEnv Not to Touch the Caller

    // This component's OWN env.theme also becomes "dark" here, not just
    // its children's—useSubEnv always includes the caller.
    useSubEnv({ theme: "dark" });
    

    If you want to configure descendants without changing anything for the current component itself, use useChildSubEnv instead. Reaching for the wrong one of the two is an easy, easy-to-miss bug: everything below the component behaves correctly, but the component's own rendering unexpectedly changes too.


    Summary

    Modern hooks represent a significant evolution in how we write components:

    • useRef creates stable references to DOM elements or any mutable value
    • useEffect handles all side effects with precise control over when they run
    • Dependency arrays make effects predictable and performant
    • Cleanup functions prevent memory leaks and resource conflicts

    The key insight is organizational: instead of thinking "what happens when the component mounts," think "what does this feature need to work properly?" Group all the logic for each feature—setup, updates, and cleanup—into focused effects.

    This approach makes your components more maintainable, testable, and easier to understand. In the next chapter, we'll explore how to communicate with the server using services, building on the solid foundation of modern hooks we've established here.

    TL;DR: useRef gives you a stable handle to a DOM element or a mutable value; useEffect runs (and cleans up) side effects based on an explicit dependency function, so you group setup and teardown for one concern in one place instead of spreading it across lifecycle hooks.

    Try it yourself: This chapter's example is available as an installable Odoo 19 addon: simplifyit_owl_book_ch11_ex1. Installation instructions are in the repository README.


    Exercises

    1. Focus-on-mount: Build a small component with a text input and a useRef. Use useEffect with an empty dependency function (() => []) to focus the input as soon as the component mounts.
    2. Fix the dependency bug: Take this broken snippet and fix it so the effect actually re-runs when state.query changes: javascript useEffect(() => { console.log("Searching for", this.state.query); }, [this.state.query]);
    3. Debounced counter: Add a useEffect that logs "Stable!" to the console only after state.count hasn't changed for 500ms (hint: setTimeout in the effect, clearTimeout in the cleanup function — the same shape as the debounced search example above).

    What's Next?

    You now have components that manage their own DOM references and side effects — but real Odoo addons rarely live in isolation from the server. Chapter 12 covers orm and rpc, the services that let your components read and write actual business data.

    in OWL Book
    Chapter 12: Server Communication with useService
    Reading the whole book? Take it with you — free, no sign-up.
    PDF EPUB All chapters
    Useful Links
    • Home
    • About us
    • Blog
    • The OWL 2.0 book
    • Help
    • Contact us
    About us

    Simplify It S.R.L. is an Odoo implementation and development firm based in La Paz, Bolivia, working with companies across Latin America and North America. We have been building on Odoo since version 6.1: custom modules, version migrations and Bolivian localization.

    We are also the authors of the OWL 2.0 book, published free chapter by chapter on our blog.

    Connect with us
    • Contact us
    • [email protected]
    • +591 65144144
    • La Paz, Bolivia
    Follow us
    Copyright © Simplify It S.R.L.
    English (US) | Español (BO)
    Powered by Odoo - Create a free website