LearnFrenzy
Sign in Take a free test

Component Communication · Lesson 8 of 25

Parent to Child in LWC: @api Properties and Methods

6 min read Updated Nov 8, 2026 Saurabh Samir

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 },
    ];
}

Result

▌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} sets isHot.
  • 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;
    }
}

Result

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';
    }
}

Result

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

  1. Create a statusPill child with an @api status that 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 {}

    Result

    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.

Next lesson

Search LearnFrenzy

Esc

Quick links