Ir al contenido
  • Inicio
  • Libro
  • Blog
  • Sobre nosotros
  • Ayuda
  • Contáctanos
  •  [email protected]
  • Inicia sesión
  • English (US) Español (BO)
  • Simplify It S.R.L.
    • Contáctanos
Simplify It S.R.L.
      • Inicio
      • Libro
      • Blog
      • Sobre nosotros
      • Ayuda
      • Contáctanos
    •  [email protected]
    • English (US) Español (BO)
    • Inicia sesión
    • Contáctanos

    Capítulo 8: Props: Comunicación de Padre a Hijo

  • Todos los blogs
  • Libro OWL
  • Capítulo 8: Props: Comunicación de Padre a Hijo
  • 25 de agosto de 2026 por
    Capítulo 8: Props: Comunicación de Padre a Hijo
    Grover Menacho

    ¿Por qué este capítulo? Casi todo reporte de bug que recibo que empieza con "el widget muestra datos desactualizados" termina en un componente que lee o modifica datos que nunca debió poseer. Las props son la regla que previene esa clase de bug por completo. ¿Qué trata de resolver Odoo con esto? El propio backend de Odoo — vistas de lista alimentando widgets de celda, vistas de formulario alimentando widgets de campo — está construido exactamente sobre este contrato de padre a hijo. Entender props es lo que te permite extender esas vistas en vez de pelear contra ellas. Aplicación en la vida real: Cualquier dashboard personalizado donde un panel de filtros controla lo que muestra un gráfico o tabla es este patrón en producción: el padre posee los filtros, los hijos solo renderizan lo que reciben.

    Hasta ahora, nuestros componentes han vivido en aislamiento, gestionando su propio estado y renderizando su propio contenido. Pero en una aplicación real, los componentes están anidados unos dentro de otros, formando una estructura similar a un árbol. Un componente Dashboard podría contener múltiples componentes Widget. Una TodoList podría contener múltiples componentes TodoItem. Un UserProfile podría contener componentes Avatar, ContactInfo, y PreferenceSettings.

    Esta composición de componentes plantea una pregunta crítica: ¿cómo se comunica un componente padre con sus hijos? ¿Cómo le dice la TodoList a cada TodoItem qué texto mostrar y si está completado? ¿Cómo pasa un Dashboard datos de configuración a sus widgets?

    La respuesta son las props (abreviatura de propiedades).

    Las props son el mecanismo principal para pasar datos de un componente padre hacia abajo a sus hijos. Piensa en ellas como argumentos que pasas a una función—el padre proporciona los datos, y el hijo los recibe y los usa. Esto crea un flujo de datos predecible y unidireccional que hace que tu aplicación sea fácil de entender, depurar y mantener.


    Entendiendo el Árbol de Componentes

    Antes de profundizar en las props, visualicemos cómo los componentes forman una jerarquía:

    Dashboard (Padre)
    |-- Header (Hijo de Dashboard)
    |   |-- Logo (Hijo de Header)
    |   |-- UserMenu (Hijo de Header)
    |-- Sidebar (Hijo de Dashboard)
    |   |-- Navigation (Hijo de Sidebar)
    |   |-- QuickActions (Hijo de Sidebar)
    |-- MainContent (Hijo de Dashboard)
        |-- TodoList (Hijo de MainContent)
        |   |-- TodoItem (Hijo de TodoList)
        |   |-- TodoItem (Hijo de TodoList)
        |   |-- AddTodoForm (Hijo de TodoList)
        |-- Statistics (Hijo de MainContent)
    

    En este árbol: - Los padres necesitan pasar datos hacia abajo a sus hijos - Los hijos reciben estos datos como props - Los datos fluyen en una dirección: hacia abajo del árbol


    Las props bajan, los callbacks vuelven a subir.{width=100%}

    Definiendo Props en el Componente Hijo

    Antes de que un componente hijo pueda recibir props, debe declarar lo que espera recibir. Esto se hace usando una definición static props en la clase del componente hijo. Definir props se considera una mejor práctica porque:

    • Documentación: Muestra claramente qué datos necesita el componente
    • Validación: OWL puede verificar que se estén pasando los tipos de datos correctos
    • Seguridad de Desarrollo: Ayuda a detectar bugs temprano en el desarrollo
    • Soporte de IDE: Mejor autocompletado y detección de errores

    Creemos un componente UserCard integral que demuestre varios tipos de props:

    En user_card.js:

    import { Component } from "@odoo/owl";
    
    export class UserCard extends Component {
      static template = "my_module.UserCard";
    
      // Definición integral de props
      static props = {
        // Props requeridas
        user: {
          type: Object,
          shape: {
            id: Number,
            name: String,
            email: String,
            avatar: { type: String, optional: true },
            role: String,
            isActive: Boolean,
            lastLogin: { type: String, optional: true }
          }
        },
    
        // Props opcionales con valores por defecto
        showDetails: { type: Boolean, optional: true },
        size: { type: String, optional: true }, // 'small', 'medium', 'large'
        theme: { type: String, optional: true },
    
        // Props de función (callbacks)
        onUserClick: { type: Function, optional: true },
        onEditUser: { type: Function, optional: true },
        onDeleteUser: { type: Function, optional: true },
    
        // Props avanzadas
        customActions: { type: Array, optional: true },
        permissions: { type: Array, optional: true },
    
        // Validación con validador personalizado
        priority: {
          type: String,
          optional: true,
          validate: (value) => ['low', 'medium', 'high'].includes(value)
        }
      };
    
      // Valores por defecto para props opcionales
      static defaultProps = {
        showDetails: false,
        size: 'medium',
        theme: 'light',
        customActions: [],
        permissions: [],
        priority: 'medium'
      };
    
      setup() {
        // Las props están disponibles como this.props
        console.log("Props de UserCard:", this.props);
      }
    
      // Método para manejar clic interno de la tarjeta
      handleCardClick() {
        if (this.props.onUserClick) {
          this.props.onUserClick(this.props.user);
        }
      }
    
      // Método para manejar acción de editar
      handleEdit() {
        if (this.props.onEditUser) {
          this.props.onEditUser(this.props.user.id);
        }
      }
    
      // Método para manejar acción de eliminar
      handleDelete() {
        if (this.props.onDeleteUser) {
          this.props.onDeleteUser(this.props.user.id);
        }
      }
    
      // Propiedades computadas basadas en 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;
      }
    }
    

    La plantilla correspondiente (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">
    
                <!-- Encabezado de la Tarjeta -->
                <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>
    
                        <!-- Nombre y Estado -->
                        <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 ? 'Activo' : 'Inactivo'"/>
                            </small>
                        </div>
                    </div>
    
                    <!-- Botones de Acción -->
                    <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>
    
                <!-- Cuerpo de la Tarjeta (Detalles Opcionales) -->
                <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>Rol:</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>Último Login:</strong> 
                                    <small class="text-muted">
                                        <t t-esc="props.user.lastLogin"/>
                                    </small>
                                </p>
                            </t>
                        </div>
    
                        <!-- Acciones Personalizadas -->
                        <t t-if="props.customActions.length > 0">
                            <div class="custom-actions mt-3">
                                <h6>Acciones Rápidas:</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>
    

    Pasando Props desde el Componente Padre

    Ahora creemos un componente padre que use nuestro UserCard y demuestre varias formas de pasar props.

    JavaScript del Componente Padre (user_dashboard.js):

    import { Component, useState } from "@odoo/owl";
    import { UserCard } from "../user_card/user_card"; // Importar el componente hijo
    
    export class UserDashboard extends Component {
      static template = "my_module.UserDashboard";
      static components = { UserCard }; // Registrar el componente hijo
    
      setup() {
        this.state = useState({
          users: [
            {
              id: 1,
              name: "Alicia Johnson",
              email: "[email protected]",
              avatar: "/web/static/img/user_menu_avatar.png",
              role: "Administrador",
              isActive: true,
              lastLogin: "2024-03-15T10:30:00Z"
            },
            {
              id: 2,
              name: "Bob Smith", 
              email: "[email protected]",
              avatar: null,
              role: "Gerente",
              isActive: true,
              lastLogin: "2024-03-14T15:45:00Z"
            },
            {
              id: 3,
              name: "Carol Brown",
              email: "[email protected]", 
              avatar: "/path/to/carol-avatar.jpg",
              role: "Empleado",
              isActive: false,
              lastLogin: "2024-03-10T09:15:00Z"
            }
          ],
    
          viewSettings: {
            showDetails: false,
            cardSize: 'medium',
            theme: 'light'
          },
    
          userPermissions: ['view', 'edit', 'delete'],
          selectedUserId: null
        });
    
        // Definir acciones personalizadas que se pasarán como props
        this.customActions = [
          {
            id: 'message',
            label: 'Enviar Mensaje',
            icon: 'fa fa-envelope',
            handler: this.sendMessageToUser.bind(this)
          },
          {
            id: 'schedule',
            label: 'Programar Reunión',
            icon: 'fa fa-calendar',
            handler: this.scheduleMeetingWith.bind(this)
          }
        ];
      }
    

    setup() solo hace dos cosas: coloca la lista de usuarios y la configuración de vista en estado reactivo, y prepara un array customActions cuyos handlers están pre-vinculados con .bind(this) para poder llamarse de forma segura desde el hijo más adelante.

    A continuación vienen los métodos que se entregarán a UserCard como callback props — esta es la mitad de "la comunicación fluye hacia arriba a través de llamadas a funciones" del patrón:

      // Manejadores de eventos que se pasarán como callbacks de props
      onUserClick(user) {
        console.log("Usuario clickeado:", user);
        this.state.selectedUserId = user.id;
    
        // Mostrar detalles del usuario en un modal o sidebar
        this.showUserDetails(user);
      }
    
      onEditUser(userId) {
        console.log("Editar usuario:", userId);
        const user = this.state.users.find(u => u.id === userId);
        if (user) {
          // Abrir modal de edición o navegar al formulario de edición
          this.openEditModal(user);
        }
      }
    
      onDeleteUser(userId) {
        console.log("Eliminar usuario:", userId);
        if (confirm("¿Estás seguro de que quieres eliminar este usuario?")) {
          this.state.users = this.state.users.filter(u => u.id !== userId);
        }
      }
    
      // Manejadores de acciones personalizadas
      sendMessageToUser(user) {
        console.log("Enviando mensaje a:", user.name);
        // Abrir interfaz de mensajería
      }
    
      scheduleMeetingWith(user) {
        console.log("Programando reunión con:", user.name);
        // Abrir interfaz de programación de calendario
      }
    

    El resto es manejo de estado de UI local — alternar configuraciones de vista y calcular listas derivadas — nada de esto involucra props, y ese es el punto: solo el padre necesita saber cómo se almacenan y filtran los usuarios.

      // Manejadores de cambios de configuración
      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';
      }
    
      // Métodos auxiliares
      showUserDetails(user) {
        // Implementación para mostrar detalles del usuario
        console.log("Mostrando detalles para:", user);
      }
    
      openEditModal(user) {
        // Implementación para abrir modal de edición
        console.log("Abriendo modal de edición para:", user);
      }
    
      // Propiedades computadas
      get activeUsers() {
        return this.state.users.filter(user => user.isActive);
      }
    
      get inactiveUsers() {
        return this.state.users.filter(user => !user.isActive);
      }
    }
    

    Plantilla del Componente Padre (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">
    
                <!-- Encabezado del Dashboard -->
                <div class="dashboard-header mb-4">
                    <div class="d-flex justify-content-between align-items-center">
                        <div>
                            <h2>Dashboard de Usuarios</h2>
                            <p class="text-muted">
                                <t t-esc="activeUsers.length"/> activos, 
                                <t t-esc="inactiveUsers.length"/> usuarios inactivos
                            </p>
                        </div>
    
                        <!-- Controles de Vista -->
                        <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 ? 'Ocultar Detalles' : 'Mostrar Detalles'"/>
                                </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')">
                                    Pequeño
                                </button>
                                <button class="btn btn-sm"
                                        t-att-class="state.viewSettings.cardSize === 'medium' ? 'btn-secondary' : 'btn-outline-secondary'"
                                        t-on-click="() => this.changeCardSize('medium')">
                                    Mediano
                                </button>
                                <button class="btn btn-sm"
                                        t-att-class="state.viewSettings.cardSize === 'large' ? 'btn-secondary' : 'btn-outline-secondary'"
                                        t-on-click="() => this.changeCardSize('large')">
                                    Grande
                                </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>
                                Tema <t t-esc="state.viewSettings.theme === 'dark' ? 'Claro' : 'Oscuro'"/>
                            </button>
                        </div>
                    </div>
                </div>
    
                <!-- Cuadrícula de Tarjetas de Usuario -->
                <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">
                                    <!-- ¡Aquí es donde se pasan las props! -->
                                    <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 === 'Administrador' ? 'high' : 'medium'"
                                    />
                                </div>
                            </div>
                        </t>
                    </div>
                </div>
    
                <!-- Estado Vacío -->
                <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 se encontraron usuarios</h4>
                        <p class="text-muted">¡Añade algunos usuarios para verlos aquí!</p>
                    </div>
                </t>
    
                <!-- Información del Usuario Seleccionado (si existe) -->
                <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>Usuario Seleccionado: <t t-esc="selectedUser.name"/></h5>
                        <p class="mb-0">Haz clic en otra tarjeta de usuario para seleccionarla.</p>
                    </div>
                </t>
            </div>
        </t>
    
    </templates>
    

    Entendiendo la Sintaxis de Paso de Props

    Analicemos las diferentes formas de pasar props:

    1. Valores Estáticos

    <!-- Pasando una cadena literal -->
    <UserCard title="'Perfil de Usuario'"/>
    
    <!-- Pasando un número -->
    <UserCard maxItems="10"/>
    
    <!-- Pasando un booleano -->
    <UserCard readonly="true"/>
    

    Importante: Nota las comillas simples dentro de las comillas dobles para cadenas. El valor del atributo es una expresión JavaScript, por lo que "'Hola'" se evalúa como la cadena "Hola".

    2. Valores Dinámicos del Estado

    <!-- Pasando propiedades del estado -->
    <UserCard user="state.currentUser"/>
    <UserCard showDetails="state.viewSettings.showDetails"/>
    <UserCard theme="state.theme"/>
    

    3. Valores Computados

    <!-- Pasando el resultado de una expresión -->
    <UserCard priority="user.role === 'Administrador' ? 'high' : 'medium'"/>
    <UserCard isSelected="state.selectedUserId === user.id"/>
    <UserCard canEdit="state.permissions.includes('edit') and user.isActive"/>
    

    4. Referencias de Función

    <!-- Pasando referencias de métodos (callbacks) -->
    <!-- El sufijo .bind vincula la función al componente padre,
         para que `this` funcione correctamente cuando el hijo la llame -->
    <UserCard onUserClick.bind="onUserClick"/>
    <UserCard onEditUser.bind="onEditUser"/>
    <UserCard onDeleteUser.bind="onDeleteUser"/>
    

    5. Objetos y Arrays

    <!-- Pasando objetos complejos -->
    <UserCard user="user" customActions="customActions"/>
    <UserCard settings="state.userSettings"/>
    <UserCard permissions="['read', 'write', 'delete']"/>
    

    Patrones Avanzados de Props

    Props Condicionales

    <!-- Solo pasar ciertas props bajo condiciones -->
    <UserCard 
        user="user"
        t-props="{
            showDetails: state.viewSettings.showDetails,
            ...(user.role === 'admin' ? { adminActions: adminActionList } : {}),
            ...(state.currentUser.id === user.id ? { highlight: true } : {})
        }"
    />
    

    Patrón de Spread Props

    <!-- Expandir un objeto completo como props -->
    <UserCard t-props="user" additionalProp="someValue"/>
    
    <!-- Combinar múltiples fuentes de props -->
    <UserCard t-props="{
        ...user,
        ...state.cardSettings,
        onUserClick: onUserClick,
        customClass: 'special-user'
    }"/>
    

    Props con Valores por Defecto

    // En el componente hijo
    static defaultProps = {
        size: 'medium',
        theme: 'light',
        showActions: true
    };
    
    // Estas props tendrán valores por defecto si no las proporciona el padre
    

    Validación y Manejo de Errores

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

    La Regla de Oro: Flujo de Datos Unidireccional

    Esto nos lleva al principio más importante sobre las props: Un componente hijo nunca, jamás, debe modificar sus propias props.

    Piensa en las props como un contrato de solo lectura del padre. El hijo puede usar los datos, mostrarlos y tomar decisiones basándose en ellos, pero no puede cambiarlos.

    Por Qué Existe Esta Regla

    1. Predictibilidad: Siempre sabes de dónde vienen los cambios de datos (el padre)
    2. Depuración: Más fácil rastrear el flujo de datos y encontrar bugs
    3. Reutilización: Los componentes hijos permanecen puros y reutilizables
    4. Rendimiento: OWL puede optimizar el renderizado cuando el flujo de datos es predecible

    Lo Que Esto Significa en la Práctica

    // NUNCA hagas esto en un componente hijo
    setup() {
      // NO modifiques las props directamente
      this.props.user.name = "Nombre Modificado"; // ¡Esto rompe el contrato!
      this.props.showDetails = true; // ¡Esto causará problemas!
    }
    
    // En su lugar, usa las props como datos de solo lectura
    setup() {
      // Usa props para inicializar estado local si es necesario
      this.state = useState({
        localShowDetails: this.props.showDetails,
        editedName: this.props.user.name
      });
    
      // O crea propiedades computadas
      this.displayName = this.props.user.name.toUpperCase();
    }
    

    Cuando un Hijo Necesita "Cambiar" Props

    Si un componente hijo necesita señalar que algo debería cambiar, debe emitir un evento hacia arriba al padre. El padre escucha el evento y decide si actualizar su propio estado, que luego fluye de vuelta hacia abajo como nuevas props.

    // Método del componente hijo
    requestNameChange(newName) {
      // No cambies props.user.name directamente
      // En su lugar, pídele al padre que lo cambie
      this.props.onNameChangeRequest?.(this.props.user.id, newName);
    }
    
    // El componente padre maneja la solicitud
    onNameChangeRequest(userId, newName) {
      const user = this.state.users.find(u => u.id === userId);
      if (user) {
        user.name = newName; // El padre actualiza su propio estado
        // Esto fluirá de vuelta hacia abajo al hijo como nuevas props
      }
    }
    

    Este patrón mantiene el flujo de datos unidireccional mientras permite que los hijos influyan en el estado del padre a través de canales de comunicación bien definidos.


    Props vs Estado: Marco de Decisión

    Entender cuándo usar props vs estado es crucial para construir componentes mantenibles:

    Usa Props Cuando:

    • Los datos vienen de un componente padre
    • Los datos son configuración o ajustes para el componente
    • Múltiples componentes necesitan los mismos datos
    • Los datos representan las "entradas" a tu componente
    • Los datos no deben ser modificados por este componente
    // Ejemplos de props
    static props = {
      userId: { type: Number },        // ID para mostrar datos
      readonly: { type: Boolean },     // Opción de configuración
      theme: { type: String },         // Configuración de toda la app
      onSave: { type: Function }       // Callback al padre
    };
    

    Usa Estado Cuando:

    • Los datos pertenecen a este componente
    • Los datos cambian basándose en interacciones del usuario en este componente
    • Los datos son temporales o específicos de la UI (como "¿está abierto el modal?")
    • Los datos se derivan de la entrada del usuario en este componente
    // Ejemplos de estado
    this.state = useState({
      inputValue: "",           // Usuario escribiendo en este componente
      isLoading: false,         // Estado de carga de este componente
      showModal: false,         // Estado de UI de este componente
      validationErrors: []      // Estado de validación de este componente
    });
    

    Patrones Comunes de Props y Mejores Prácticas

    1. Patrón de Props Callback

    <!-- El padre proporciona callbacks para que el hijo se comunique de vuelta -->
    <TodoItem 
      todo="todo"
      onToggle="(id) => this.toggleTodo(id)"
      onEdit="(id, newText) => this.editTodo(id, newText)"
      onDelete="(id) => this.deleteTodo(id)"
    />
    

    2. Patrón de Props de Configuración

    <!-- El padre configura el comportamiento del hijo -->
    <DataTable 
      data="state.users"
      columns="tableColumns"
      sortable="true"
      filterable="true"
      pageSize="10"
      showPagination="true"
    />
    

    3. Patrón de Render Props

    <!-- El padre proporciona lógica de renderizado -->
    <Modal 
      isOpen="state.showModal"
      onClose="closeModal"
      renderContent="() => this.renderModalContent()"
      renderFooter="() => this.renderModalFooter()"
    />
    

    4. Patrón de Componente Compuesto

    <!-- Múltiples componentes hijos relacionados -->
    <Card>
      <CardHeader title="'Perfil de Usuario'" actions="headerActions"/>
      <CardBody content="userDetails"/>
      <CardFooter buttons="footerButtons"/>
    </Card>
    

    Depurando Props

    Problemas Comunes de Props y Soluciones

    Problema 1: Props que no se actualizan

    <!-- Problema: Pasar un valor estático en lugar de estado reactivo -->
    <UserCard user="staticUserObject"/>
    
    <!-- Solución: Pasar estado reactivo -->
    <UserCard user="state.currentUser"/>
    

    Problema 2: Props undefined o tipo incorrecto

    // Verificar props en el setup del componente
    setup() {
      console.log("Props recibidas:", this.props);
    
      // Validar props críticas
      if (!this.props.user) {
        console.error("¡UserCard requiere una prop user!");
      }
    }
    

    Problema 3: Props de función que no funcionan

    <!-- Problema: Llamar función en lugar de pasar referencia -->
    <UserCard onUserClick="onUserClick()"/>
    
    <!-- Solución: Pasar referencia de función (vinculada con .bind) -->
    <UserCard onUserClick.bind="onUserClick"/>
    

    Plantilla de Depuración de Props

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

    Consideraciones de Rendimiento

    Optimizando Props para Rendimiento

    1. Evita Crear Objetos en Plantillas:
    <!-- Malo: Crea nuevo objeto en cada render -->
    <UserCard settings="{ theme: 'dark', size: 'large' }"/>
    
    <!-- Bueno: Almacenar objeto en estado del componente o método -->
    <UserCard settings="cardSettings"/>
    
    1. Memoriza Cálculos Complejos (memoization significa cachear el resultado de un cálculo costoso para no recalcularlo cuando las entradas no han cambiado):
    // Calcula props costosas una vez
    get expensiveComputedProp() {
      // Memoriza o cachea este cálculo
      return this.state.data.reduce(...);
    }
    
    1. Usa Referencias de Función Estables:
    // Crea métodos enlazados una vez en setup
    setup() {
      this.boundHandleClick = this.handleClick.bind(this);
    }
    
    
    <!-- Usa la referencia estable en la plantilla -->
    <UserCard onUserClick="boundHandleClick"/>
    

    Errores Comunes

    Error 1: Mutar Props Directamente

    // NO HAGAS ESTO: rompe el contrato de flujo de datos unidireccional
    this.props.user.name = "Nuevo Nombre";
    

    Las props pertenecen al padre. Si el hijo necesita datos distintos, cópialos a un estado local (useState) o pide al padre que los cambie mediante una callback prop.

    Error 2: Omitir la Validación con static props

    Sin una declaración static props, OWL no puede detectar por ti una prop faltante o con el tipo equivocado — el error aparece después como un fallo confuso en tiempo de ejecución en vez de un mensaje claro al renderizar. Declara siempre static props en los componentes que reciben datos de un padre.

    Error 3: Olvidar .bind en las Callback Props

    <!-- `this` dentro de onEditUser será undefined -->
    <UserCard onEditUser="onEditUser"/>
    
    <!-- Correcto -->
    <UserCard onEditUser.bind="onEditUser"/>
    

    Error 4: No Definir defaultProps para Props Opcionales

    Si una prop es optional: true pero el componente igual asume que tiene un valor (ej. this.props.size.toUpperCase()), un padre que la omita hará que el hijo falle. Acompaña cada prop opcional con una entrada razonable en static defaultProps.


    Ejercicios

    Ejercicio 1: Validar y Definir Valores por Defecto

    Toma el componente UserCard de este capítulo y añade una nueva prop opcional badgeText (String). Dale un valor por defecto "Member" mediante static defaultProps, y muéstrala junto al nombre del usuario solo cuando props.showDetails sea verdadero.

    Ejercicio 2: Disciplina de Solo Lectura

    Escribe un pequeño componente hijo que reciba una prop counter (Number). Intenta mutar this.props.counter dentro de un método y confirma (en la consola del navegador) que OWL te advierte o que el cambio no persiste. Luego corrígelo copiando la prop a un estado local con useState en setup().

    Ejercicio 3: Callback Prop Desde Cero

    Construye un componente RatingWidget que reciba una prop value y una callback prop onRate. Cuando el usuario haga clic en una de 5 estrellas, el componente debe llamar a this.props.onRate(starIndex) — nunca debe modificar this.props.value directamente. Conéctalo a un padre que guarde la calificación actual en useState.


    Las props son los bloques de construcción fundamentales de la comunicación entre componentes en OWL. Al dominar los patrones de props y entender el principio de flujo de datos unidireccional, construirás aplicaciones que son predecibles, mantenibles y escalables.

    TL;DR: Las props fluyen en una sola dirección, de padre a hijo; un hijo nunca muta sus propias props, y declara static props/static defaultProps para que los errores salgan a la luz temprano en vez de como bugs confusos en tiempo de ejecución.

    Pruébalo tú mismo: El ejemplo de este capítulo está disponible como addon instalable de Odoo 19: simplifyit_owl_book_ch8_ex1. Las instrucciones de instalación están en el README del repositorio.

    ¿Qué Sigue?

    Ya cubrimos cómo fluyen los datos hacia abajo, de padre a hijo. En el próximo capítulo, aprenderemos sobre la otra mitad de la comunicación padre-hijo: cómo los hijos pueden comunicarse de vuelta a sus padres usando callback props.

    en Libro OWL
    Capítulo 9: Manejo de Eventos: Comunicación de Hijo a Padre
    Reading the whole book? Take it with you — free, no sign-up.
    PDF EPUB All chapters
    Enlaces útiles
    • Inicio
    • Sobre nosotros
    • Blog
    • El libro de OWL 2.0
    • Ayuda
    • Contáctanos
    Sobre nosotros

    Simplify It S.R.L. es una firma de implementación y desarrollo Odoo con base en La Paz, Bolivia, que trabaja con empresas de Latinoamérica y Norteamérica. Desarrollamos sobre Odoo desde la versión 6.1: módulos a medida, migraciones de versión y localización boliviana.

    También somos autores del libro de OWL 2.0, que publicamos gratis capítulo a capítulo en nuestro blog.

    Conecta con nosotros
    • Contáctanos
    • [email protected]
    • +591 65144144
    • La Paz, Bolivia
    Síguenos
    Copyright © Simplify It S.R.L.
    English (US) | Español (BO)
    Con la tecnología de Odoo - Cree un sitio web gratuito