Skip to main content

How to Build a Lightning Datatable in LWC: Step-by-Step Guide with Apex Wire Service

In plain words: The <lightning-datatable> is a built-in UI component in Salesforce Lightning Web Components (LWC) that lets you display rows of records in a structured grid with built-in column formatting, row actions, and sorting.

Displaying tabular data efficiently is a core requirement for enterprise Salesforce applications. Using the base component <lightning-datatable> combined with Apex wire adapters delivers a responsive, standard-compliant UI that handles pagination, inline data formatting, and event actions with minimal custom CSS.

1. Apex Controller Implementation

To supply data to the LWC wire service, the Apex method must be annotated with @AuraEnabled(cacheable=true). Caching improves client-side performance by enabling the Lightning Data Service (LDS) cache.

Step 1: Apex Controller (AccountController.cls)
Exposing a SOQL query to fetch Account records:
public with sharing class AccountController {
    @AuraEnabled(cacheable=true)
    public static List<Account> getAccountRecords() {
        return [
            SELECT Id, Name, Industry, Phone, Website 
            FROM Account 
            WITH USER_MODE 
            LIMIT 50
        ];
    }
}

2. LWC HTML Template Implementation

The HTML template wraps the datatable inside a standard <lightning-card> container. The datatable binds to properties configured in the JavaScript controller.

Step 2: Component Template (accountDatatable.html)
<template>
    <lightning-card title="Account Records" icon-name="standard:account">
        <div class="slds-m-around_medium">
            <lightning-datatable
                key-field="Id"
                data={data}
                columns={columns}
                onrowaction={handleRowAction}
                hide-checkbox-column="true">
            </lightning-datatable>
        </div>
    </lightning-card>
</template>

3. LWC JavaScript Controller

The JavaScript controller imports the Apex method, defines the column metadata structure (labels, field bindings, and types), and handles wire service callbacks and notifications.

Step 3: Component Controller (accountDatatable.js)
import { LightningElement, wire } from 'lwc';
import { refreshApex } from '@salesforce/apex';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import getAccountRecords from '@salesforce/apex/AccountController.getAccountRecords';

const COLUMNS = [
    { label: 'Account Name', fieldName: 'Name', type: 'text' },
    { label: 'Industry', fieldName: 'Industry', type: 'text' },
    { label: 'Phone', fieldName: 'Phone', type: 'phone' },
    { label: 'Website', fieldName: 'Website', type: 'url', typeAttributes: { target: '_blank' } }
];

export default class AccountDatatable extends LightningElement {
    data;
    error;
    columns = COLUMNS;
    wiredAccountResult;

    @wire(getAccountRecords)
    wiredAccountData(result) {
        this.wiredAccountResult = result;
        const { data, error } = result;

        if (data) {
            this.data = data;
            this.error = undefined;
        } else if (error) {
            this.error = error;
            this.data = undefined;
            this.showToast('Error', 'An error occurred while retrieving account records.', 'error');
        }
    }

    handleRowAction(event) {
        const row = event.detail.row;
        // Handle row action logic (e.g., navigation, edit modal)
    }

    showToast(title, message, variant) {
        const event = new ShowToastEvent({
            title: title,
            message: message,
            variant: variant
        });
        this.dispatchEvent(event);
    }

    refreshTable() {
        return refreshApex(this.wiredAccountResult);
    }
}
360 Component Architecture Card:
  • key-field: Mandatory unique identifier attribute (e.g., Id) used for DOM rendering and performance tracking.
  • columns: Array of objects configuring column header titles, field mapping, data types, and custom formatting parameters.
  • @wire Decorator: Reactive service connecting client-side properties to Apex methods or Lightning Data Service.
  • refreshApex(): Clears cached wire data and re-provisions fresh records from the server.

4. Key Implementation Rules & Common Traps

Developer Trap: Passing Deconstructed Objects to refreshApex
A common mistake is passing this.data into refreshApex(). refreshApex requires the full immutable wire response object (assigned as this.wiredAccountResult = result above), not just the unboxed data payload.
Core Rule: Always declare cacheable=true on Apex methods bound to @wire, and enforce Object-Level & Field-Level Security using WITH USER_MODE in SOQL queries.
  • Flatten Nested Objects: Standard datatables do not evaluate nested relationship fields (e.g., Account.Owner.Name) directly. Flatten relationship data in JavaScript before binding to this.data.
  • Set Appropriate Column Types: Specify types like url, currency, date, or phone in column definitions so Lightning Design System (SLDS) formats values automatically.
  • Provide User Feedback: Always handle the error block in wire functions using ShowToastEvent to communicate network or authorization failures gracefully.

Summary

The <lightning-datatable> provides a high-performance, accessible way to render Salesforce records in LWC. By integrating an Apex controller with the @wire service, passing full result objects to refreshApex(), and defining structured column definitions, you can build reliable and responsive table views for any custom workflow.