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
{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
- Predictability: You always know where data changes come from (the parent)
- Debugging: Easier to trace data flow and find bugs
- Reusability: Child components remain pure and reusable
- 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
- 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"/>
- 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(...);
}
- 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.