LearnFrenzy
Sign in Take a free test

Working with Salesforce Data · Lesson 8 of 25

Array Methods for Salesforce Records in LWC

6 min read Updated Nov 8, 2026 Saurabh Samir

Apex and wire adapters give you arrays of records. Before they reach the screen you almost always need to reshape them: flatten Account.Name for a datatable, total the amounts, group by stage, find the selected row. Modern array methods do each of these in one readable line – and, importantly for LWC, they return new arrays instead of changing the old one.

In this lesson

  • Loops: for…of and when you still need them
  • map, filter, find, some, every, includes
  • reduce and Object.groupBy
  • Sorting without mutating
  • Flattening relationship fields for lightning-datatable

The data for this lesson

const opportunities = [
    { Id: '006A', Name: 'Acme renewal', Amount: 120000, StageName: 'Negotiation', Account: { Name: 'Acme' } },
    { Id: '006B', Name: 'Globex pilot', Amount: 15000, StageName: 'Prospecting', Account: { Name: 'Globex' } },
    { Id: '006C', Name: 'Initech upsell', Amount: 48000, StageName: 'Closed Won', Account: { Name: 'Initech' } },
    { Id: '006D', Name: 'Acme add-on', Amount: 9000, StageName: 'Prospecting', Account: null },
];

// for…of: when you need break/continue or await inside the loop
for (const opp of opportunities) {
    if (opp.StageName === 'Closed Won') break;
    console.log(`${opp.Name}: ${opp.Amount}`);
}

// classic for with index – rarely needed; entries() gives you both
for (const [index, opp] of opportunities.entries()) {
    if (index > 1) break;
    console.log(index, opp.Id);
}

Output

Acme renewal: 120000
Globex pilot: 15000
0 006A
1 006B

forEach works too, but you can't break out of it and it ignores await. For transforming data, reach for the methods below instead of loops that push into an array.

map, filter, find, some, every

const opportunities = [
    { Id: '006A', Name: 'Acme renewal', Amount: 120000, StageName: 'Negotiation' },
    { Id: '006B', Name: 'Globex pilot', Amount: 15000, StageName: 'Prospecting' },
    { Id: '006C', Name: 'Initech upsell', Amount: 48000, StageName: 'Closed Won' },
];

const names = opportunities.map((o) => o.Name);
const open = opportunities.filter((o) => !o.StageName.startsWith('Closed'));
const selected = opportunities.find((o) => o.Id === '006B');
const index = opportunities.findIndex((o) => o.Id === '006C');

console.log(names);
console.log(open.map((o) => o.Id));
console.log(selected?.Name, index);
console.log(opportunities.some((o) => o.Amount > 100000), opportunities.every((o) => o.Amount > 10000));
console.log(['Hot', 'Warm'].includes('Cold'), opportunities.at(-1).Name);

Output

[ 'Acme renewal', 'Globex pilot', 'Initech upsell' ]
[ '006A', '006B' ]
Globex pilot 2
true true
false Initech upsell
MethodReturnsTypical LWC use
mapnew array, same lengthadd display fields, build options
filternew array, matching itemsclient-side filters, remove a deleted row
find / findIndexfirst match / its index (or undefined / -1)the clicked or selected record
some / everybooleanenable a button, validation
reduceany single valuetotals, lookup maps

reduce and groupBy

const opportunities = [
    { Id: '006A', Amount: 120000, StageName: 'Negotiation' },
    { Id: '006B', Amount: 15000, StageName: 'Prospecting' },
    { Id: '006C', Amount: 48000, StageName: 'Closed Won' },
    { Id: '006D', Amount: 9000, StageName: 'Prospecting' },
];

const total = opportunities.reduce((sum, o) => sum + (o.Amount ?? 0), 0);   // always pass the start value
console.log(total);

// array → lookup map by Id (fast access by key)
const byId = opportunities.reduce((map, o) => ({ ...map, [o.Id]: o }), {});
console.log(byId['006C'].StageName);

// group records by a field (supported in all current browsers)
const byStage = Object.groupBy(opportunities, (o) => o.StageName);
console.log(Object.keys(byStage));
console.log(byStage.Prospecting.map((o) => o.Id));

const summary = Object.entries(byStage).map(([stage, items]) => `${stage}: ${items.length}`);
console.log(summary.join(' | '));

Output

192000
Closed Won
[ 'Negotiation', 'Prospecting', 'Closed Won' ]
[ '006B', '006D' ]
Negotiation: 1 | Prospecting: 2 | Closed Won: 1

Sorting without surprises

const accounts = [
    { Name: 'globex', AnnualRevenue: 5000000 },
    { Name: 'Acme', AnnualRevenue: null },
    { Name: 'Émile & Co', AnnualRevenue: 800000 },
];

console.log([10, 9, 100].sort());                       // default sort compares as strings!
console.log([10, 9, 100].toSorted((a, b) => a - b));    // numbers: give a compare function

const byName = accounts.toSorted((a, b) => a.Name.localeCompare(b.Name, 'en', { sensitivity: 'base' }));
console.log(byName.map((a) => a.Name));

// nulls last, highest revenue first
const byRevenue = accounts.toSorted((a, b) => (b.AnnualRevenue ?? -Infinity) - (a.AnnualRevenue ?? -Infinity));
console.log(byRevenue.map((a) => a.Name));
console.log(accounts[0].Name);                          // toSorted left the original alone

Output

[ 10, 100, 9 ]
[ 9, 10, 100 ]
[ 'Acme', 'Émile & Co', 'globex' ]
[ 'globex', 'Émile & Co', 'Acme' ]
globex

sort() sorts the array in place, which mutates your component's data (and fails on read-only wire data). toSorted() returns a sorted copy; on older code you'll see the equivalent [...list].sort(…).

Flattening records for lightning-datatable

lightning-datatable can't read a relationship path like Account.Name in a column's fieldName. The standard fix is to map each record into a flat row:

// opportunityTable.js
import { LightningElement, wire } from 'lwc';
import getOpenOpportunities from '@salesforce/apex/OpportunityController.getOpenOpportunities';

const COLUMNS = [
    { label: 'Opportunity', fieldName: 'Name' },
    { label: 'Account', fieldName: 'AccountName' },
    { label: 'Amount', fieldName: 'Amount', type: 'currency' },
    { label: 'Size', fieldName: 'Size' },
];

export default class OpportunityTable extends LightningElement {
    columns = COLUMNS;
    rows = [];
    totalText = '';

    @wire(getOpenOpportunities)
    wired({ data, error }) {
        if (data) {
            this.rows = data.map((opp) => ({
                ...opp,
                AccountName: opp.Account?.Name ?? '—',
                Size: opp.Amount >= 100000 ? 'Large' : 'Standard',
            }));
            const total = data.reduce((sum, opp) => sum + (opp.Amount ?? 0), 0);
            this.totalText = `${data.length} open · ${total.toLocaleString('en-US')} total`;
        } else if (error) {
            this.rows = [];
            this.totalText = 'Could not load opportunities';
        }
    }
}
<!-- opportunityTable.html -->
<template>
    <lightning-datatable key-field="Id" data={rows} columns={columns} hide-checkbox-column></lightning-datatable>
    <p>{totalText}</p>
</template>

Result

Opportunity | Account | Amount | Size
Acme renewal | Acme | $120,000.00 | Large
Globex pilot | Globex | $15,000.00 | Standard
Add-on | — | $9,000.00 | Standard
3 open · 144,000 total

Notice that map builds a brand-new object for each row with spread. That's not just style: wire data is read-only, so adding opp.AccountName = … directly to the received records would throw. The next lesson explains why.

Practice

Try it yourself

  1. Build combobox options { label, value } from an array of Users, showing "Name (Title)" and sorted by name.

    Show answer

    users.map((u) => ({ label: `${u.Name} (${u.Title ?? 'No title'})`, value: u.Id })).toSorted((a, b) => a.label.localeCompare(b.label))

  2. Remove the row with Id id from this.rows the LWC way.

    Show answer

    this.rows = this.rows.filter((r) => r.Id !== id); – a new array assigned to the field, so the table re-renders.

Quick check

What does find return when nothing matches?

undefined (findIndex returns -1).

Why is [10, 9, 100].sort() wrong for numbers?

Without a compare function, items are compared as strings.

Why can't a datatable column use fieldName "Account.Name"?

It reads top-level properties only; flatten the field with map.

Summary

  • map, filter, find, some and reduce cover most record shaping.
  • Object.groupBy groups; reduce totals and builds lookup maps.
  • Use toSorted (or a copy) with a compare function; never sort wire data in place.
  • Flatten relationship fields into new row objects for lightning-datatable.

Finished this lesson?

Mark it complete to track your progress, then carry on with the next lesson.

Next lesson

Popular