Component Communication · Lesson 8 of 25
Parent to Child in LWC: @api Properties and Methods
Real interfaces are built from components inside components: a deal board containing deal cards, a form containing address inputs. Data flows down from parent to child through public properties marked with @api, and a parent can call a child's public methods. This lesson covers both, plus the rules that keep components predictable.
In this lesson
- Public properties with
@api - Rules: children must not change their @api properties
- Reacting to changes with a getter/setter pair
- Calling child methods with
lwc:ref
Public properties
<!-- dealCard.html -->
<template>
<div class="slds-box slds-m-bottom_x-small">
<p><strong>{name}</strong> – {formattedAmount}</p>
<p>{stage}{hotLabel}</p>
</div>
</template>
// dealCard.js
import { LightningElement, api } from 'lwc';
export default class DealCard extends LightningElement {
@api name;
@api amount = 0;
@api stage = 'Prospecting';
@api isHot = false; // boolean attributes: default must be false
get formattedAmount() {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(this.amount);
}
get hotLabel() {
return this.isHot ? ' 🔥' : '';
}
}
<!-- dealBoard.html -->
<template>
<lightning-card title="Pipeline">
<template for:each={deals} for:item="deal">
<c-deal-card key={deal.id} name={deal.name} amount={deal.amount} stage={deal.stage} is-hot={deal.hot}></c-deal-card>
</template>
</lightning-card>
</template>
// dealBoard.js
import { LightningElement } from 'lwc';
export default class DealBoard extends LightningElement {
deals = [
{ id: 1, name: 'Acme renewal', amount: 120000, stage: 'Negotiation', hot: true },
{ id: 2, name: 'Globex expansion', amount: 45000, stage: 'Proposal', hot: false },
];
}
▌Pipeline Acme renewal – $120,000 Negotiation 🔥 Globex expansion – $45,000 Proposal
- The child declares what it accepts with
@api; the parent sets those properties as attributes, in kebab-case:is-hot={deal.hot}setsisHot. - When the parent's data changes, the new values flow down and the child re-renders.
- Boolean public properties should default to
false– an attribute's presence means true.
Children must not change their @api properties
A public property belongs to the parent. If a child assigns to it, the value gets out of sync with the parent and LWC logs an error. To let the user change something, the child keeps its own copy in a private field – or asks the parent to change it with an event (next lesson):
import { LightningElement, api } from 'lwc';
export default class QuantityPicker extends LightningElement {
@api initialQuantity = 1;
quantity; // private, editable copy
connectedCallback() {
this.quantity = this.initialQuantity;
}
increase() {
this.quantity++; // ✓ private field
// this.initialQuantity++; // ✗ never assign to an @api property from inside
}
}
Reacting to changes: getter and setter
When a child needs to do work whenever a public property changes – parse it, normalise it, reset internal state – define the property as a getter/setter pair. Put @api on one of them (by convention the getter):
<!-- tagList.html -->
<template>
<p>{tagCount} tags:</p>
<template for:each={tags} for:item="tag">
<lightning-badge key={tag} label={tag}></lightning-badge>
</template>
</template>
// tagList.js
import { LightningElement, api } from 'lwc';
export default class TagList extends LightningElement {
tags = [];
_raw = '';
@api
get value() {
return this._raw;
}
set value(text) { // runs every time the parent sets value
this._raw = text ?? '';
this.tags = [...new Set(this._raw.split(',').map((t) => t.trim().toLowerCase()).filter(Boolean))];
}
get tagCount() {
return this.tags.length;
}
}
3 tags: (lwc) (apex) (flow) › parent sets value = "SOQL, DML" 2 tags: (soql) (dml)
The setter cleaned the input – trimmed, lower-cased and removed the duplicate "lwc" – every time the parent passed a new value.
Calling a child's method
Sometimes a parent needs the child to do something: reset a form, focus a field, refresh its data. The child exposes a method with @api, and the parent gets a reference to the child with lwc:ref:
<!-- stopwatchDisplay.html -->
<template>
<p>Laps recorded: {laps}</p>
</template>
// stopwatchDisplay.js
import { LightningElement, api } from 'lwc';
export default class StopwatchDisplay extends LightningElement {
laps = 0;
@api
recordLap() {
this.laps++;
return this.laps;
}
@api
reset() {
this.laps = 0;
}
}
<!-- stopwatchPanel.html -->
<template>
<c-stopwatch-display lwc:ref="display"></c-stopwatch-display>
<lightning-button label="Lap" onclick={handleLap}></lightning-button>
<lightning-button label="Reset" onclick={handleReset}></lightning-button>
<p>{message}</p>
</template>
// stopwatchPanel.js
import { LightningElement } from 'lwc';
export default class StopwatchPanel extends LightningElement {
message = '';
handleLap() {
const count = this.refs.display.recordLap(); // call the child's public method
this.message = `Lap ${count} saved`;
}
handleReset() {
this.refs.display.reset();
this.message = 'Cleared';
}
}
Laps recorded: 0 [Lap] [Reset] › click “Lap” Laps recorded: 1 [Lap] [Reset] Lap 1 saved › click “Lap” Laps recorded: 2 [Lap] [Reset] Lap 2 saved › click “Reset” Laps recorded: 0 [Lap] [Reset] Cleared
lwc:ref instead of querySelector
this.refs.name is the modern way to reach an element in your own template. It's clearer and faster than this.template.querySelector('c-stopwatch-display'), and doesn't break when markup changes. Refs are available after the component renders – not in the constructor or connectedCallback.
Practice
Try it yourself
Create a
statusPillchild with an@api statusthat shows "● Active" or "○ Inactive", and use it twice in a parent.Show solution
<!-- statusPill.html --> <template> <span>{label}</span> </template>// statusPill.js import { LightningElement, api } from 'lwc'; export default class StatusPill extends LightningElement { @api status; get label() { return this.status === 'active' ? '● Active' : '○ Inactive'; } }<!-- userRow.html --> <template> <p>Asha <c-status-pill status="active"></c-status-pill></p> <p>Ravi <c-status-pill status="inactive"></c-status-pill></p> </template>// userRow.js import { LightningElement } from 'lwc'; export default class UserRow extends LightningElement {}Asha ● Active Ravi ○ Inactive
Quick check
How does a parent set a child's recordName property?
With the attribute record-name={value}.
Should a child assign to its own @api property?
No – keep a private copy or ask the parent with an event.
How does a parent call a child method?
Mark the method @api, add lwc:ref="name" to the child, and call this.refs.name.method().
Summary
- @api properties pass data down; set them as kebab-case attributes.
- Children treat @api values as read-only; use a getter/setter to react to changes.
- @api methods plus lwc:ref let a parent tell a child to act.
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.