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 8: Props: Parent-to-Child Communication

  • All Blogs
  • OWL Book
  • Chapter 8: Props: Parent-to-Child Communication
  • August 25, 2026 by
    Chapter 8: Props: Parent-to-Child Communication
    Grover Menacho

    Why this chapter? Almost every bug report I get that starts with "the widget shows stale data" traces back to a component reading or mutating data it was never supposed to own. Props are the rule that prevents that class of bug entirely. What is Odoo trying to solve with this? Odoo's own backend — list views feeding cell widgets, form views feeding field widgets — is built on exactly this parent-to-child contract. Understanding props is what lets you extend those views instead of fighting them. Real-world application: Any custom dashboard where a filter panel controls what a chart or table displays is this pattern in production: the parent owns the filters, the children just render whatever they're handed.

    So far, our components have lived in isolation, managing their own state and rendering their own content. But in a real application, components are nested inside each other, forming a tree-like structure. A Dashboard component might contain multiple Widget components. A TodoList might contain multiple TodoItem components. A UserProfile might contain Avatar, ContactInfo, and PreferenceSettings components.

    This component composition raises a critical question: how does a parent component communicate with its children? How does the TodoList tell each TodoItem what text to display and whether it's completed? How does a Dashboard pass configuration data to its widgets?

    The answer is props (short for properties).

    Props are the primary mechanism for passing data from a parent component down to its children. Think of them as arguments you pass to a function—the parent provides the data, and the child receives and uses it. This creates a predictable, one-way data flow that makes your application easy to understand, debug, and maintain.


    Understanding the Component Tree

    Before diving into props, let's visualize how components form a hierarchy:

    Dashboard (Parent)
    |-- Header (Child of Dashboard)
    |   |-- Logo (Child of Header)
    |   |-- UserMenu (Child of Header)
    |-- Sidebar (Child of Dashboard)
    |   |-- Navigation (Child of Sidebar)
    |   |-- QuickActions (Child of Sidebar)
    |-- MainContent (Child of Dashboard)
        |-- TodoList (Child of MainContent)
        |   |-- TodoItem (Child of TodoList)
        |   |-- TodoItem (Child of TodoList)
        |   |-- AddTodoForm (Child of TodoList)
        |-- Statistics (Child of MainContent)
    

    In this tree: - Parents need to pass data down to their children - Children receive this data as props - Data flows in one direction: down the tree


    Props flow down, callbacks travel back up.{width=100%}

    Defining Props in the Child Component

    Before a child component can receive props, it must declare what it expects to receive. This is done using a static props definition in the child component's class. Defining props is considered a best practice because:

    • Documentation: It clearly shows what data the component needs
    • Validation: OWL can check that the right data types are being passed
    • Development Safety: Helps catch bugs early in development
    • IDE Support: Better autocomplete and error detection

    Let's create a comprehensive UserCard component that demonstrates various prop types:

    In user_card.js:

    import { Component } from "@odoo/owl";
    
    export class UserCard extends Component {
      static template = "my_module.UserCard";
    
      // Comprehensive props definition
      static props = {
        // Required props
        user: {
          type: Object,
          shape: {
            id: Number,
            name: String,
            email: String,
            avatar: { type: String, optional: true },
            role: String,
            isActive: Boolean,
            lastLogin: { type: String, optional: true }
          }
        },
    
        // Optional props with defaults
        showDetails: { type: Boolean, optional: true },
        size: { type: String, optional: true }, // 'small', 'medium', 'large'
        theme: { type: String, optional: true },
    
        // Function props (callbacks)
        onUserClick: { type: Function, optional: true },
        onEditUser: { type: Function, optional: true },
        onDeleteUser: { type: Function, optional: true },
    
        // Advanced props
        customActions: { type: Array, optional: true },
        permissions: { type: Array, optional: true },
    
        // Validation with custom validator
        priority: {
          type: String,
          optional: true,
          validate: (value) => ['low', 'medium', 'high'].includes(value)
        }
      };
    
      // Default values for optional props
      static defaultProps = {
        showDetails: false,
        size: 'medium',
        theme: 'light',
        customActions: [],
        permissions: [],
        priority: 'medium'
      };
    
      setup() {
        // Props are available as this.props
        console.log("UserCard props:", this.props);
      }
    
      // Method to handle internal card click
      handleCardClick() {
        if (this.props.onUserClick) {
          this.props.onUserClick(this.props.user);
        }
      }
    
      // Method to handle edit action
      handleEdit() {
        if (this.props.onEditUser) {
          this.props.onEditUser(this.props.user.id);
        }
      }
    
      // Method to handle delete action
      handleDelete() {
        if (this.props.onDeleteUser) {
          this.props.onDeleteUser(this.props.user.id);
        }
      }
    
      // Computed properties based on props
      get cardSizeClass() {
        const sizeClasses = {
          small: 'user-card-sm',
          medium: 'user-card-md', 
          large: 'user-card-lg'
        };
        return sizeClasses[this.props.size] || sizeClasses.medium;
      }
    
      get priorityBadgeClass() {
        const priorityClasses = {
          low: 'badge-secondary',
          medium: 'badge-warning',
          high: 'badge-danger'
        };
        return priorityClasses[this.props.priority] || priorityClasses.medium;
      }
    
      get canEdit() {
        return this.props.permissions.includes('edit') && this.props.onEditUser;
      }
    
      get canDelete() {
        return this.props.permissions.includes('delete') && this.props.onDeleteUser;
      }
    }
    

    The corresponding template (user_card.xml):

    <?xml version="1.0" encoding="UTF-8"?>
    <templates xml:space="preserve">
    
        <t t-name="my_module.UserCard" owl="1">
            <div class="user-card card"
                 t-att-class="cardSizeClass"
                 t-att-data-theme="props.theme"
                 t-on-click="handleCardClick">
    
                <!-- Card Header -->
                <div class="card-header d-flex justify-content-between align-items-center">
                    <div class="user-basic-info d-flex align-items-center">
                        <!-- Avatar -->
                        <div class="user-avatar me-3">
                            <t t-if="props.user.avatar">
                                <img t-att-src="props.user.avatar" 
                                     t-att-alt="props.user.name + ' avatar'"
                                     class="rounded-circle"
                                     width="40" height="40"/>
                            </t>
                            <t t-else="">
                                <div class="avatar-placeholder rounded-circle bg-secondary d-flex align-items-center justify-content-center"
                                     style="width: 40px; height: 40px;">
                                    <i class="fa fa-user text-white"></i>
                                </div>
                            </t>
                        </div>
    
                        <!-- Name and Status -->
                        <div>
                            <h5 class="card-title mb-1">
                                <t t-esc="props.user.name"/>
                                <t t-if="props.priority !== 'medium'">
                                    <span t-att-class="'badge ms-2 ' + priorityBadgeClass">
                                        <t t-esc="props.priority"/>
                                    </span>
                                </t>
                            </h5>
                            <small t-att-class="props.user.isActive ? 'text-success' : 'text-muted'">
                                <i t-att-class="props.user.isActive ? 'fa fa-circle' : 'fa fa-circle-o'"></i>
                                <t t-esc="props.user.isActive ? 'Active' : 'Inactive'"/>
                            </small>
                        </div>
                    </div>
    
                    <!-- Action Buttons -->
                    <div class="card-actions">
                        <t t-if="canEdit">
                            <button class="btn btn-sm btn-outline-primary me-1" 
                                    t-on-click.stop="handleEdit">
                                <i class="fa fa-edit"></i>
                            </button>
                        </t>
                        <t t-if="canDelete">
                            <button class="btn btn-sm btn-outline-danger" 
                                    t-on-click.stop="handleDelete">
                                <i class="fa fa-trash"></i>
                            </button>
                        </t>
                    </div>
                </div>
    
                <!-- Card Body (Optional Details) -->
                <t t-if="props.showDetails">
                    <div class="card-body">
                        <div class="user-details">
                            <p class="mb-2">
                                <strong>Email:</strong> 
                                <a t-attf-href="mailto:{{props.user.email}}">
                                    <t t-esc="props.user.email"/>
                                </a>
                            </p>
                            <p class="mb-2">
                                <strong>Role:</strong> 
                                <span class="badge bg-info">
                                    <t t-esc="props.user.role"/>
                                </span>
                            </p>
                            <t t-if="props.user.lastLogin">
                                <p class="mb-2">
                                    <strong>Last Login:</strong> 
                                    <small class="text-muted">
                                        <t t-esc="props.user.lastLogin"/>
                                    </small>
                                </p>
                            </t>
                        </div>
    
                        <!-- Custom Actions -->
                        <t t-if="props.customActions.length > 0">
                            <div class="custom-actions mt-3">
                                <h6>Quick Actions:</h6>
                                <t t-foreach="props.customActions" t-as="action" t-key="action.id">
                                    <button class="btn btn-sm btn-outline-secondary me-1 mb-1"
                                            t-on-click="() => action.handler(props.user)">
                                        <i t-att-class="action.icon"></i>
                                        <t t-esc="action.label"/>
                                    </button>
                                </t>
                            </div>
                        </t>
                    </div>
                </t>
            </div>
        </t>
    
    </templates>
    

    Passing Props from the Parent Component

    Now let's create a parent component that uses our UserCard and demonstrates various ways to pass props.

    Parent Component's JavaScript (user_dashboard.js):

    import { Component, useState } from "@odoo/owl";
    import { UserCard } from "../user_card/user_card"; // Import the child component
    
    export class UserDashboard extends Component {
      static template = "my_module.UserDashboard";
      static components = { UserCard }; // Register the child component
    
      setup() {
        this.state = useState({
          users: [
            {
              id: 1,
              name: "Alice Johnson",
              email: "[email protected]",
              avatar: "/web/static/img/user_menu_avatar.png",
              role: "Administrator",
              isActive: true,
              lastLogin: "2024-03-15T10:30:00Z"
            },
            {
              id: 2,
              name: "Bob Smith", 
              email: "[email protected]",
              avatar: null,
              role: "Manager",
              isActive: true,
              lastLogin: "2024-03-14T15:45:00Z"
            },
            {
              id: 3,
              name: "Carol Brown",
              email: "[email protected]", 
              avatar: "/path/to/carol-avatar.jpg",
              role: "Employee",
              isActive: false,
              lastLogin: "2024-03-10T09:15:00Z"
            }
          ],
    
          viewSettings: {
            showDetails: false,
            cardSize: 'medium',
            theme: 'light'
          },
    
          userPermissions: ['view', 'edit', 'delete'],
          selectedUserId: null
        });
    
        // Define custom actions that will be passed as props
        this.customActions = [
          {
            id: 'message',
            label: 'Send Message',
            icon: 'fa fa-envelope',
            handler: this.sendMessageToUser.bind(this)
          },
          {
            id: 'schedule',
            label: 'Schedule Meeting',
            icon: 'fa fa-calendar',
            handler: this.scheduleMeetingWith.bind(this)
          }
        ];
      }
    

    setup() only does two things: it puts the user list and view settings into reactive state, and it prepares a customActions array whose handlers are pre-bound with .bind(this) so they can be called safely from the child later.

    Next come the methods that will be handed down to UserCard as callback props—this is the "communication flows up through function calls" half of the pattern:

      // Event handlers that will be passed as prop callbacks
      onUserClick(user) {
        console.log("User clicked:", user);
        this.state.selectedUserId = user.id;
    
        // Show user details in a modal or sidebar
        this.showUserDetails(user);
      }
    
      onEditUser(userId) {
        console.log("Edit user:", userId);
        const user = this.state.users.find(u => u.id === userId);
        if (user) {
          // Open edit modal or navigate to edit form
          this.openEditModal(user);
        }
      }
    
      onDeleteUser(userId) {
        console.log("Delete user:", userId);
        if (confirm("Are you sure you want to delete this user?")) {
          this.state.users = this.state.users.filter(u => u.id !== userId);
        }
      }
    
      // Custom action handlers
      sendMessageToUser(user) {
        console.log("Sending message to:", user.name);
        // Open messaging interface
      }
    
      scheduleMeetingWith(user) {
        console.log("Scheduling meeting with:", user.name);
        // Open calendar scheduling interface
      }
    

    The rest is local UI state management—toggling view settings and computing derived lists—none of it involves props at all, which is the point: only the parent needs to know how users are stored and filtered.

      // Settings change handlers
      toggleDetails() {
        this.state.viewSettings.showDetails = !this.state.viewSettings.showDetails;
      }
    
      changeCardSize(size) {
        this.state.viewSettings.cardSize = size;
      }
    
      toggleTheme() {
        this.state.viewSettings.theme = this.state.viewSettings.theme === 'light' ? 'dark' : 'light';
      }
    
      // Helper methods
      showUserDetails(user) {
        // Implementation for showing user details
        console.log("Showing details for:", user);
      }
    
      openEditModal(user) {
        // Implementation for opening edit modal
        console.log("Opening edit modal for:", user);
      }
    
      // Computed properties
      get activeUsers() {
        return this.state.users.filter(user => user.isActive);
      }
    
      get inactiveUsers() {
        return this.state.users.filter(user => !user.isActive);
      }
    }
    

    Parent Component's Template (user_dashboard.xml):

    <?xml version="1.0" encoding="UTF-8"?>
    <templates xml:space="preserve">
    
        <t t-name="my_module.UserDashboard" owl="1">
            <div class="user-dashboard p-4">
    
                <!-- Dashboard Header -->
                <div class="dashboard-header mb-4">
                    <div class="d-flex justify-content-between align-items-center">
                        <div>
                            <h2>User Dashboard</h2>
                            <p class="text-muted">
                                <t t-esc="activeUsers.length"/> active, 
                                <t t-esc="inactiveUsers.length"/> inactive users
                            </p>
                        </div>
    
                        <!-- View Controls -->
                        <div class="view-controls">
                            <div class="btn-group me-3" role="group">
                                <button class="btn btn-sm"
                                        t-att-class="state.viewSettings.showDetails ? 'btn-primary' : 'btn-outline-primary'"
                                        t-on-click="toggleDetails">
                                    <i class="fa fa-info-circle"></i>
                                    <t t-esc="state.viewSettings.showDetails ? 'Hide Details' : 'Show Details'"/>
                                </button>
                            </div>
    
                            <div class="btn-group me-3" role="group">
                                <button class="btn btn-sm"
                                        t-att-class="state.viewSettings.cardSize === 'small' ? 'btn-secondary' : 'btn-outline-secondary'"
                                        t-on-click="() => this.changeCardSize('small')">
                                    Small
                                </button>
                                <button class="btn btn-sm"
                                        t-att-class="state.viewSettings.cardSize === 'medium' ? 'btn-secondary' : 'btn-outline-secondary'"
                                        t-on-click="() => this.changeCardSize('medium')">
                                    Medium
                                </button>
                                <button class="btn btn-sm"
                                        t-att-class="state.viewSettings.cardSize === 'large' ? 'btn-secondary' : 'btn-outline-secondary'"
                                        t-on-click="() => this.changeCardSize('large')">
                                    Large
                                </button>
                            </div>
    
                            <button class="btn btn-sm btn-outline-secondary" t-on-click="toggleTheme">
                                <i t-att-class="state.viewSettings.theme === 'dark' ? 'fa fa-sun' : 'fa fa-moon'"></i>
                                <t t-esc="state.viewSettings.theme === 'dark' ? 'Light' : 'Dark'"/> Theme
                            </button>
                        </div>
                    </div>
                </div>
    
                <!-- User Cards Grid -->
                <div class="users-grid">
                    <div class="row">
                        <t t-foreach="state.users" t-as="user" t-key="user.id">
                            <div t-att-class="state.viewSettings.cardSize === 'small' ? 'col-md-4' : state.viewSettings.cardSize === 'large' ? 'col-12' : 'col-md-6'">
                                <div class="mb-3">
                                    <!-- This is where props are passed! -->
                                    <UserCard 
                                        user="user"
                                        showDetails="state.viewSettings.showDetails"
                                        size="state.viewSettings.cardSize"
                                        theme="state.viewSettings.theme"
                                        onUserClick.bind="onUserClick"
                                        onEditUser.bind="onEditUser"
                                        onDeleteUser.bind="onDeleteUser"
                                        customActions="customActions"
                                        permissions="state.userPermissions"
                                        priority="user.role === 'Administrator' ? 'high' : 'medium'"
                                    />
                                </div>
                            </div>
                        </t>
                    </div>
                </div>
    
                <!-- Empty State -->
                <t t-if="state.users.length === 0">
                    <div class="empty-state text-center py-5">
                        <i class="fa fa-users fa-3x text-muted mb-3"></i>
                        <h4 class="text-muted">No users found</h4>
                        <p class="text-muted">Add some users to see them here!</p>
                    </div>
                </t>
    
                <!-- Selected User Info (if any) -->
                <t t-if="state.selectedUserId">
                    <t t-set="selectedUser" t-value="state.users.find(u => u.id === state.selectedUserId)"/>
                    <div class="selected-user-info mt-4 p-3 bg-light rounded">
                        <h5>Selected User: <t t-esc="selectedUser.name"/></h5>
                        <p class="mb-0">Click on another user card to select them.</p>
                    </div>
                </t>
            </div>
        </t>
    
    </templates>
    

    Understanding Prop Passing Syntax

    Let's break down the different ways to pass props:

    1. Static Values

    <!-- Passing a string literal -->
    <UserCard title="'User Profile'"/>
    
    <!-- Passing a number -->
    <UserCard maxItems="10"/>
    
    <!-- Passing a boolean -->
    <UserCard readonly="true"/>
    

    Important: Notice the single quotes inside double quotes for strings. The attribute value is a JavaScript expression, so "'Hello'" evaluates to the string "Hello".

    2. Dynamic Values from State

    <!-- Passing state properties -->
    <UserCard user="state.currentUser"/>
    <UserCard showDetails="state.viewSettings.showDetails"/>
    <UserCard theme="state.theme"/>
    

    3. Computed Values

    <!-- Passing the result of an expression -->
    <UserCard priority="user.role === 'Administrator' ? 'high' : 'medium'"/>
    <UserCard isSelected="state.selectedUserId === user.id"/>
    <UserCard canEdit="state.permissions.includes('edit') and user.isActive"/>
    

    4. Function References

    <!-- Passing method references (callbacks) -->
    <!-- The .bind suffix binds the function to the parent component,
         so `this` works correctly when the child calls it -->
    <UserCard onUserClick.bind="onUserClick"/>
    <UserCard onEditUser.bind="onEditUser"/>
    <UserCard onDeleteUser.bind="onDeleteUser"/>
    

    5. Objects and Arrays

    <!-- Passing complex objects -->
    <UserCard user="user" customActions="customActions"/>
    <UserCard settings="state.userSettings"/>
    <UserCard permissions="['read', 'write', 'delete']"/>
    

    Advanced Props Patterns

    Conditional Props

    <!-- Only pass certain props under conditions -->
    <UserCard 
        user="user"
        t-props="{
            showDetails: state.viewSettings.showDetails,
            ...(user.role === 'admin' ? { adminActions: adminActionList } : {}),
            ...(state.currentUser.id === user.id ? { highlight: true } : {})
        }"
    />
    

    Spread Props Pattern

    <!-- Spread an entire object as props -->
    <UserCard t-props="user" additionalProp="someValue"/>
    
    <!-- Combine multiple prop sources -->
    <UserCard t-props="{
        ...user,
        ...state.cardSettings,
        onUserClick: onUserClick,
        customClass: 'special-user'
    }"/>
    

    Props with Defaults

    // In the child component
    static defaultProps = {
        size: 'medium',
        theme: 'light',
        showActions: true
    };
    
    // These props will have default values if not provided by parent
    

    Validation and Error Handling

    static props = {
        user: {
            type: Object,
            validate: (user) => {
                if (!user.id || !user.name) {
                    throw new Error("User must have id and name");
                }
                return true;
            }
        },
        priority: {
            type: String,
            optional: true,
            validate: (value) => ['low', 'medium', 'high'].includes(value)
        }
    };
    

    The Golden Rule: One-Way Data Flow

    This brings us to the most important principle about props: A child component should never, ever modify its own props.

    Think of props as a read-only contract from the parent. The child can use the data, display it, and make decisions based on it, but it cannot change it.

    Why This Rule Exists

    1. Predictability: You always know where data changes come from (the parent)
    2. Debugging: Easier to trace data flow and find bugs
    3. Reusability: Child components remain pure and reusable
    4. Performance: OWL can optimize rendering when data flow is predictable

    What This Means in Practice

    // NEVER do this in a child component
    setup() {
      // DON'T modify props directly
      this.props.user.name = "Modified Name"; // This breaks the contract!
      this.props.showDetails = true; // This will cause issues!
    }
    
    // Instead, use props as read-only data
    setup() {
      // Use props to initialize local state if needed
      this.state = useState({
        localShowDetails: this.props.showDetails,
        editedName: this.props.user.name
      });
    
      // Or create computed properties
      this.displayName = this.props.user.name.toUpperCase();
    }
    

    When a Child Needs to "Change" Props

    If a child component needs to signal that something should change, it should emit an event up to the parent. The parent listens for the event and decides whether to update its own state, which then flows back down as new props.

    // Child component method
    requestNameChange(newName) {
      // Don't change props.user.name directly
      // Instead, ask the parent to change it
      this.props.onNameChangeRequest?.(this.props.user.id, newName);
    }
    
    // Parent component handles the request
    onNameChangeRequest(userId, newName) {
      const user = this.state.users.find(u => u.id === userId);
      if (user) {
        user.name = newName; // Parent updates its own state
        // This will flow back down to the child as new props
      }
    }
    

    This pattern maintains the one-way data flow while still allowing children to influence parent state through well-defined communication channels.


    Props vs State: Decision Framework

    Understanding when to use props vs state is crucial for building maintainable components:

    Use Props When:

    • Data comes from a parent component
    • Data is configuration or settings for the component
    • Multiple components need the same data
    • Data represents the "inputs" to your component
    • Data shouldn't be modified by this component
    // Props examples
    static props = {
      userId: { type: Number },        // ID to display data for
      readonly: { type: Boolean },     // Configuration option
      theme: { type: String },         // App-wide setting
      onSave: { type: Function }       // Callback to parent
    };
    

    Use State When:

    • Data belongs to this component
    • Data changes based on user interactions in this component
    • Data is temporary or UI-specific (like "is modal open?")
    • Data is derived from user input in this component
    // State examples
    this.state = useState({
      inputValue: "",           // User typing in this component
      isLoading: false,         // This component's loading state
      showModal: false,         // This component's UI state
      validationErrors: []      // This component's validation state
    });
    

    Common Props Patterns and Best Practices

    1. Callback Props Pattern

    <!-- Parent provides callbacks for child to communicate back -->
    <TodoItem 
      todo="todo"
      onToggle="(id) => this.toggleTodo(id)"
      onEdit="(id, newText) => this.editTodo(id, newText)"
      onDelete="(id) => this.deleteTodo(id)"
    />
    

    2. Configuration Props Pattern

    <!-- Parent configures child behavior -->
    <DataTable 
      data="state.users"
      columns="tableColumns"
      sortable="true"
      filterable="true"
      pageSize="10"
      showPagination="true"
    />
    

    3. Render Props Pattern

    <!-- Parent provides rendering logic -->
    <Modal 
      isOpen="state.showModal"
      onClose="closeModal"
      renderContent="() => this.renderModalContent()"
      renderFooter="() => this.renderModalFooter()"
    />
    

    4. Compound Component Pattern

    <!-- Multiple related child components -->
    <Card>
      <CardHeader title="'User Profile'" actions="headerActions"/>
      <CardBody content="userDetails"/>
      <CardFooter buttons="footerButtons"/>
    </Card>
    

    Debugging Props

    Common Props Issues and Solutions

    Issue 1: Props not updating

    <!-- Problem: Passing a static value instead of reactive state -->
    <UserCard user="staticUserObject"/>
    
    <!-- Solution: Pass reactive state -->
    <UserCard user="state.currentUser"/>
    

    Issue 2: Props undefined or wrong type

    // Check props in component setup
    setup() {
      console.log("Received props:", this.props);
    
      // Validate critical props
      if (!this.props.user) {
        console.error("UserCard requires a user prop!");
      }
    }
    

    Issue 3: Function props not working

    <!-- Problem: Calling function instead of passing reference -->
    <UserCard onUserClick="onUserClick()"/>
    
    <!-- Solution: Pass function reference (bound with .bind) -->
    <UserCard onUserClick.bind="onUserClick"/>
    

    Props Debugging Template

    <!-- Temporary debugging section -->
    <div class="props-debug" style="background: #f8f9fa; padding: 10px; margin: 10px 0; border-radius: 4px;">
      <strong>Props Debug:</strong>
      <pre t-esc="JSON.stringify(props, null, 2)"/>
    </div>
    

    Performance Considerations

    Optimizing Props for Performance

    1. Avoid Creating Objects in Templates:
    <!-- Bad: Creates new object every render -->
    <UserCard settings="{ theme: 'dark', size: 'large' }"/>
    
    <!-- Good: Store object in component state or method -->
    <UserCard settings="cardSettings"/>
    
    1. Memoize Complex Calculations (memoization means caching the result of an expensive computation so it isn't recalculated when the inputs haven't changed):
    // Calculate expensive props once
    get expensiveComputedProp() {
      // Memoize or cache this calculation
      return this.state.data.reduce(...);
    }
    
    1. Use Stable Function References:
    // Create bound methods once in setup
    setup() {
      this.boundHandleClick = this.handleClick.bind(this);
    }
    
    
    <!-- Use the stable reference in template -->
    <UserCard onUserClick="boundHandleClick"/>
    

    Common Pitfalls

    Pitfall 1: Mutating Props Directly

    // DON'T: this breaks the one-way data flow contract
    this.props.user.name = "New Name";
    

    Props belong to the parent. If the child needs different data, copy it into local state (useState) or ask the parent to change it via a callback prop.

    Pitfall 2: Skipping static props Validation

    Without a static props declaration, OWL can't catch a missing or mistyped prop for you—the bug surfaces later as a confusing runtime error instead of a clear message at render time. Always declare static props on components that receive data from a parent.

    Pitfall 3: Forgetting .bind on Callback Props

    <!-- `this` inside onEditUser will be undefined -->
    <UserCard onEditUser="onEditUser"/>
    
    <!-- Correct -->
    <UserCard onEditUser.bind="onEditUser"/>
    

    Pitfall 4: No defaultProps for Optional Props

    If a prop is optional: true but the component still assumes it has a value (e.g. this.props.size.toUpperCase()), a parent that omits it will crash the child. Pair every optional prop with a sensible entry in static defaultProps.


    Exercises

    Exercise 1: Validate and Default

    Take the UserCard component from this chapter and add a new optional prop badgeText (String). Give it a default value of "Member" via static defaultProps, and render it next to the user's name only when props.showDetails is true.

    Exercise 2: Read-Only Discipline

    Write a small child component that receives a counter prop (Number). Try mutating this.props.counter inside a method and confirm (via the browser console) that OWL warns you or that the change doesn't persist. Then fix it by copying the prop into local state with useState on setup().

    Exercise 3: Callback Prop From Scratch

    Build a RatingWidget component that receives a value prop and an onRate callback prop. When the user clicks one of 5 stars, the component should call this.props.onRate(starIndex)—it should never modify this.props.value itself. Wire it up from a parent that stores the current rating in useState.


    Props are the fundamental building blocks of component communication in OWL. By mastering props patterns and understanding the one-way data flow principle, you'll build applications that are predictable, maintainable, and scalable.

    TL;DR: Props flow one way, parent to child; a child never mutates its own props, and declares static props/static defaultProps so mistakes surface early instead of as confusing runtime bugs.

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

    What's Next?

    We've covered how data flows down, from parent to child. In the next chapter, we'll learn about the other half of parent-child communication: how children can communicate back to their parents using callback props.

    in OWL Book
    Chapter 9: Handling Events: Child-to-Parent Communication
    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