How to Build Your First Production-Grade Lightning Web Component the Right Way

How to Build Your First Production-Grade Lightning Web Component the Right Way
Component lifecycle. Parent-child communication. Wire service. Error handling. Testing. A complete walkthrough from the Hokoriam team on LWC patterns that work in production orgs.

Lightning Web Components are the standard for modern Salesforce UI development. Built on native web standards, custom elements, shadow DOM, ECMAScript modules they are faster than Aura components and more maintainable than Visualforce. This guide walks through building a real, production-ready LWC from file structure to deployed component, covering every pattern you need for serious development work.

The File Structure Every LWC Developer Needs to Understand

Every LWC component lives in its own folder. The folder name is the component name. Three files are required for any functional component – HTML template, JavaScript controller, and metadata XML. Additional files for styles, tests, and documentation are added as needed.

The HTML Template – Reactive Bindings and Conditional Rendering

accountCard.html
<template>
    <!-- Conditional rendering - only shows when account is loaded -->
    <template lwc:if={account}>
        <lightning-card title={account.Name} icon-name="standard:account">
            <div class="slds-p-around_medium">
                <p>Industry: {account.Industry}</p>
                <p>Phone: {account.Phone}</p>
                <p>Annual Revenue: {formattedRevenue}</p>

                <!-- Iterate contacts -->
                <template for:each={contacts} for:item="contact">
                    <c-contact-row
                        key={contact.Id}
                        contact={contact}
                        oncontactselected={handleContactSelected}>
                    </c-contact-row>
                </template>
            </div>
        </lightning-card>
    </template>

    <!-- Error state -->
    <template lwc:elseif={error}>
        <p class="slds-text-color_error">{error}</p>
    </template>

    <!-- Loading state -->
    <template lwc:else>
        <lightning-spinner></lightning-spinner>
    </template>
</template>

The JavaScript Controller – Wire Service, Properties, and Event Handling

accountCard.js
import { LightningElement, api, wire, track } from 'lwc';
import { getRecord, getFieldValue } from 'lightning/uiRecordApi';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import getAccountContacts from '@salesforce/apex/AccountController.getContacts';
import NAME_FIELD from '@salesforce/schema/Account.Name';
import INDUSTRY_FIELD from '@salesforce/schema/Account.Industry';
import PHONE_FIELD from '@salesforce/schema/Account.Phone';
import REVENUE_FIELD from '@salesforce/schema/Account.AnnualRevenue';

const FIELDS = [NAME_FIELD, INDUSTRY_FIELD, PHONE_FIELD, REVENUE_FIELD];

export default class AccountCard extends LightningElement {
    // @api - public property, can be set by parent component
    @api recordId;

    // @track - private reactive state (array/object deep changes)
    @track contacts = [];

    error;

    // Wire service - auto-fetches data reactively when recordId changes
    @wire(getRecord, { recordId: '$recordId', fields: FIELDS })
    wiredAccount({ data, error }) {
        if (data) {
            this.account = {
                Name: getFieldValue(data, NAME_FIELD),
                Industry: getFieldValue(data, INDUSTRY_FIELD),
                Phone: getFieldValue(data, PHONE_FIELD),
                AnnualRevenue: getFieldValue(data, REVENUE_FIELD)
            };
            this.error = undefined;
        } else if (error) {
            this.error = error.body?.message || 'Failed to load account';
            this.account = undefined;
        }
    }

    // Wire to Apex - reactive, cached
    @wire(getAccountContacts, { accountId: '$recordId' })
    wiredContacts({ data, error }) {
        if (data) this.contacts = data;
        else if (error) console.error(error);
    }

    // Getter - derived value, no getter clutter for display logic
    get formattedRevenue() {
        if (!this.account?.AnnualRevenue) return 'N/A';
        return new Intl.NumberFormat('en-US', {
            style: 'currency', currency: 'USD', maximumFractionDigits: 0
        }).format(this.account.AnnualRevenue);
    }

    // Handle event from child component
    handleContactSelected(event) {
        const contactId = event.detail.contactId;
        this.dispatchEvent(new ShowToastEvent({
            title: 'Contact Selected',
            message: `Contact ${contactId} selected`,
            variant: 'success'
        }));
    }
}

The Metadata XML – Targets and Visibility

accountCard.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>63.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <!-- Where this component can be placed -->
        <target>lightning__RecordPage</target>
        <target>lightning__AppPage</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__RecordPage">
            <!-- Expose recordId as a configurable property -->
            <property name="recordId" type="String" />
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>

Parent-Child Communication – Events and Properties

The rule: Parent passes data down to child via @api properties. Child communicates up to parent by dispatching custom events. Never communicate sideways between sibling components – use a shared parent or a message channel.

contactRow.js - child component fires custom event
import { LightningElement, api } from 'lwc';

export default class ContactRow extends LightningElement {
    @api contact; // Received from parent

    handleSelect() {
        // Fire event upward - parent listens with oncontactselected
        this.dispatchEvent(new CustomEvent('contactselected', {
            detail: { contactId: this.contact.Id },
            bubbles: true  // bubble through shadow DOM if needed
        }));
    }
}

Always test your component in isolation. Use @salesforce/jest-config with lwc-jest to unit test components without deploying. Testing wire adapters, event dispatching, and conditional rendering in Jest catches issues before they reach sandbox.


Custom LWC Components Built by Hokoriam

Our team has built custom Lightning Web Components across distributor portals, B2B commerce storefronts, service consoles, and internal operations dashboards. From simple record cards to complex multi-step flows if your org needs bespoke UI that standard components cannot deliver, that is what we build.

See Our Work →

This site uses cookies to offer you a better browsing experience. By browsing this website, you agree to our use of cookies.