Skip to main content

How to Build a Dynamic Dependent Picklist in Salesforce LWC (uiObjectInfoApi)

In plain words: A dependent picklist in Salesforce is a pair of dropdown menus where the choices available in the second (child/dependent) dropdown automatically change based on the choice you select in the first (parent/controlling) dropdown.

Rather than writing complex, custom Apex schema classes or hardcoding options, Lightning Web Components (LWC) provide built-in wire adapters through lightning/uiObjectInfoApi. These adapters (getObjectInfo and getPicklistValues) handle the entire controlling-dependent mapping, caching, and record-type filtering automatically.

Prerequisites

  • A Salesforce Developer Edition, Scratch Org, or active Sandbox.
  • Salesforce CLI (sf) installed and authenticated.
  • A standard or custom object with configured controlling and dependent picklist fields (e.g., standard Account.Industry controlling a custom Sub_Industry__c).

Step 1: Create the Component Bundle

CLI Generation Command:
sf lightning generate component -n dependentPicklistDemo -d force-app/main/default/lwc --type lwc

Step 2: Build the HTML Template

In dependentPicklistDemo.html, declare two lightning-combobox elements. The dependent combobox remains disabled until a controlling parent selection is made:

<template>
    <lightning-card title="Dynamic Dependent Picklist Demo" icon-name="standard:picklist_type">
        <div class="slds-p-around_medium">
            
            <div class="slds-grid slds-gutters slds-wrap">
                <!-- Controlling (Parent) Picklist -->
                <div class="slds-col slds-size_1-of-1 slds-medium-size_1-of-2 slds-m-bottom_small">
                    <lightning-combobox
                        name="controllingField"
                        label="Industry (Controlling)"
                        placeholder="Select Industry"
                        options={parentOptions}
                        value={selectedParentValue}
                        onchange={handleParentChange}>
                    </lightning-combobox>
                </div>

                <!-- Dependent (Child) Picklist -->
                <div class="slds-col slds-size_1-of-1 slds-medium-size_1-of-2 slds-m-bottom_small">
                    <lightning-combobox
                        name="dependentField"
                        label="Sub-Category (Dependent)"
                        placeholder="Select Sub-Category"
                        options={childOptions}
                        value={selectedChildValue}
                        onchange={handleChildChange}
                        disabled={isChildDisabled}>
                    </lightning-combobox>
                </div>
            </div>

            <!-- Selection Summary -->
            <template lwc:if={selectedParentValue}>
                <div class="slds-box slds-theme_shade slds-m-top_medium">
                    <p>Selected Controlling Value: <strong>{selectedParentValue}</strong></p>
                    <p>Selected Dependent Value: <strong>{selectedChildValue}</strong></p>
                </div>
            </template>

        </div>
    </lightning-card>
</template>

Step 3: Implement the UI Object Info JavaScript Wire Logic

In dependentPicklistDemo.js, wire the default record type using getObjectInfo, fetch both picklists with getPicklistValues, and parse the dependent mapping object returned by Salesforce:

import { LightningElement, wire } from 'lwc';
import { getObjectInfo, getPicklistValues } from 'lightning/uiObjectInfoApi';

import ACCOUNT_OBJECT from '@salesforce/schema/Account';
import INDUSTRY_FIELD from '@salesforce/schema/Account.Industry';
import SUB_CATEGORY_FIELD from '@salesforce/schema/Account.Type'; // Replace with your dependent field schema

export default class DependentPicklistDemo extends LightningElement {
    parentOptions = [];
    childOptions = [];
    selectedParentValue = '';
    selectedChildValue = '';

    // Cached dependent metadata bundle
    dependentPicklistData;

    get isChildDisabled() {
        return !this.selectedParentValue || this.childOptions.length === 0;
    }

    // Step 1: Retrieve Object Info to get Default RecordTypeId
    @wire(getObjectInfo, { objectApiName: ACCOUNT_OBJECT })
    objectInfo;

    // Step 2: Fetch Controlling (Parent) Picklist Values
    @wire(getPicklistValues, {
        recordTypeId: '$objectInfo.data.defaultRecordTypeId',
        fieldApiName: INDUSTRY_FIELD
    })
    wiredParentPicklist({ data, error }) {
        if (data) {
            this.parentOptions = data.values.map(item => ({
                label: item.label,
                value: item.value
            }));
        } else if (error) {
            console.error('Error loading parent picklist values:', error);
        }
    }

    // Step 3: Fetch Dependent (Child) Picklist Values & Controller Mapping
    @wire(getPicklistValues, {
        recordTypeId: '$objectInfo.data.defaultRecordTypeId',
        fieldApiName: SUB_CATEGORY_FIELD
    })
    wiredChildPicklist({ data, error }) {
        if (data) {
            this.dependentPicklistData = data;
        } else if (error) {
            console.error('Error loading dependent picklist values:', error);
        }
    }

    handleParentChange(event) {
        this.selectedParentValue = event.target.value;
        this.selectedChildValue = '';
        this.filterDependentOptions();
    }

    handleChildChange(event) {
        this.selectedChildValue = event.target.value;
    }

    filterDependentOptions() {
        if (!this.dependentPicklistData || !this.selectedParentValue) {
            this.childOptions = [];
            return;
        }

        // Get key index of the selected controlling value
        const controllerIndex = this.dependentPicklistData.controllerValues[this.selectedParentValue];

        // Filter valid child values using validFor array indices
        this.childOptions = this.dependentPicklistData.values
            .filter(item => item.validFor.includes(controllerIndex))
            .map(item => ({
                label: item.label,
                value: item.value
            }));
    }
}
Warning Trap: Never invoke getPicklistValues imperatively inside event handlers like handleParentChange. getPicklistValues is a standard wire adapter, not an imperative Apex method. Wire both picklist fields upfront, and filter the dependent choices on the client side using the validFor array and controllerValues map.
360 Architecture Summary:
  • validFor Mapping: Each dependent entry in getPicklistValues returns an array of controller indices (validFor) designating which parent choices enable it.
  • controllerValues Map: Maps each parent string value to an integer index key (e.g., {"Banking": 0, "Consulting": 1}).
  • Record Type Awareness: Dynamically inherits record-type-specific field dependencies without extra SOQL or Apex code.
  • Client Caching: All picklist metadata is provisioned and cached via Lightning Data Service (LDS) on initial page load.

Step 4: Expose and Deploy the Component

Update dependentPicklistDemo.js-meta.xml to expose the component in Lightning App Builder:

<?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__AppPage</target>
        <target>lightning__RecordPage</target>
        <target>lightning__HomePage</target>
    </targets>
</LightningComponentBundle>
Deployment & Testing:
# Deploy to the default authenticated org
sf project deploy start

# Open target org in browser
sf org open
  • Open Setup > Lightning App Builder.
  • Drag dependentPicklistDemo onto any Record or App page, then save and activate.
  • Choose a value in the Controlling picklist and verify that the Dependent picklist instantly filters and enables its matching sub-options.
Core Takeaway: Using lightning/uiObjectInfoApi wire adapters provides zero-Apex, record-type-aware dependent picklist filtering with built-in client-side caching.