Skip to main content

How to Build a Field History Tracking LWC in Salesforce

Tracking field history in Salesforce is crucial for maintaining data compliance and auditing record updates over time. While standard related lists show field updates, building a custom Lightning Web Component (LWC) allows you to filter, format, and present old versus new values dynamically directly on record pages.

In plain words: A Field History LWC queries history tracking objects (like AccountHistory or OpportunityFieldHistory) via Apex, returning a clear audit table of modified fields, previous values, new values, user names, and timestamps.

Prerequisites

  • Basic understanding of LWC JavaScript structure and Apex wire service patterns.
  • Field History Tracking enabled on target objects in Salesforce Setup.
  • Salesforce DX CLI configured or Developer Console access.

Step 1: Create the LWC Bundle

Generate a new Lightning Web Component named fieldHistoryComponent using your IDE or terminal:

sf force lightning component create --type lwc --componentname fieldHistoryComponent --outputdir force-app/main/default/lwc

Step 2: Build the HTML Template Layout

In fieldHistoryComponent.html, combine a lightning-combobox for field selection with a lightning-datatable to show history records:

<!-- fieldHistoryComponent.html -->
<template>
    <lightning-card title="Field History Tracking" icon-name="standard:history">
        <div class="slds-p-around_medium">
            <lightning-combobox
                name="fieldSelect"
                label="Select Field to Audit"
                value={selectedField}
                options={fieldOptions}
                onchange={handleFieldChange}>
            </lightning-combobox>

            <div class="slds-m-top_medium">
                <template if:true={historyData}>
                    <lightning-datatable
                        key-field="id"
                        data={historyData}
                        columns={columns}
                        hide-checkbox-column>
                    </lightning-datatable>
                </template>

                <template if:true={error}>
                    <p class="slds-text-color_error">Unable to load field history.</p>
                </template>
            </div>
        </div>
    </lightning-card>
</template>

Step 3: Define JavaScript Controller Logic

In fieldHistoryComponent.js, declare field options, column definitions, and wire the Apex method reactively based on user interaction:

// fieldHistoryComponent.js
import { LightningElement, api, wire, track } from 'lwc';
import getFieldHistory from '@salesforce/apex/FieldHistoryController.getFieldHistory';

const COLUMNS = [
    { label: 'Old Value', fieldName: 'OldValue', type: 'text' },
    { label: 'New Value', fieldName: 'NewValue', type: 'text' },
    { label: 'Modified By', fieldName: 'CreatedByName', type: 'text' },
    { label: 'Modified Date', fieldName: 'CreatedDate', type: 'date', typeAttributes: {
        year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit'
    }}
];

export default class FieldHistoryComponent extends LightningElement {
    @api recordId;
    @track selectedField = 'Account.Name';
    @track historyData;
    @track error;

    columns = COLUMNS;

    fieldOptions = [
        { label: 'Account Name', value: 'Account.Name' },
        { label: 'Type', value: 'Account.Type' },
        { label: 'Annual Revenue', value: 'Account.AnnualRevenue' }
    ];

    @wire(getFieldHistory, { recordId: '$recordId', fieldName: '$selectedField' })
    wiredHistory({ data, error }) {
        if (data) {
            // Flatten parent metadata relationship fields for datatable binding
            this.historyData = data.map(record => ({
                id: record.Id,
                OldValue: record.OldValue,
                NewValue: record.NewValue,
                CreatedByName: record.CreatedBy ? record.CreatedBy.Name : '',
                CreatedDate: record.CreatedDate
            }));
            this.error = undefined;
        } else if (error) {
            this.error = error;
            this.historyData = undefined;
        }
    }

    handleFieldChange(event) {
        this.selectedField = event.detail.value;
    }
}
Developer Trap: Datatable Relationship Flaws: Datatables cannot automatically resolve nested SOQL relationships like CreatedBy.Name. Always flatten parent relationships during array transformation (e.g. mapping record.CreatedBy.Name to a top-level key like CreatedByName) before passing data to lightning-datatable!

Step 4: Create the Apex Controller

Create an Apex class named FieldHistoryController.cls to query historical object tracking tables securely:

Apex SOQL Adapter: Standard object field histories are queried from object-specific tracking tables (e.g., AccountHistory) using Field, OldValue, and NewValue fields.
// FieldHistoryController.cls
public with sharing class FieldHistoryController {
    @AuraEnabled(cacheable=true)
    public static List<AccountHistory> getFieldHistory(Id recordId, String fieldName) {
        if (String.isBlank(recordId)) {
            return new List<AccountHistory>();
        }

        // Extract field name without object prefix if passed
        String fieldApi = fieldName.contains('.') ? fieldName.substringAfter('.') : fieldName;

        return [
            SELECT Id, Field, OldValue, NewValue, CreatedBy.Name, CreatedDate 
            FROM AccountHistory 
            WHERE AccountId = :recordId AND Field = :fieldApi
            WITH USER_MODE
            ORDER BY CreatedDate DESC
            LIMIT 100
        ];
    }
}

Step 5: Expose Target Metadata and Test

Update fieldHistoryComponent.js-meta.xml to expose the component for Record Page deployment:

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>60.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__RecordPage</target>
    </targets>
</LightningComponentBundle>
Key Summary: Field History Component
  • Data Source: Queries object history tables (e.g., AccountHistory, ContactHistory).
  • Prerequisites: Field History Tracking must be enabled on target object fields in Setup.
  • Apex Best Practice: Secure queries using WITH USER_MODE and mark methods @AuraEnabled(cacheable=true) for wire optimization.
  • UI Component: Uses lightning-datatable with flattened relationship properties for seamless rendering.

Conclusion

Building a custom Field History LWC gives developers granular control over auditing presentation within Salesforce. By pairing reactive wire handlers with efficient Apex SOQL adapters, you deliver a clean, interactive history tracking experience directly on Lightning record pages.