Working with Salesforce Data · Lesson 8 of 25
Array Methods for Salesforce Records in LWC
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);
}
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);
[ 'Acme renewal', 'Globex pilot', 'Initech upsell' ] [ '006A', '006B' ] Globex pilot 2 true true false Initech upsell
| Method | Returns | Typical LWC use |
|---|---|---|
map | new array, same length | add display fields, build options |
filter | new array, matching items | client-side filters, remove a deleted row |
find / findIndex | first match / its index (or undefined / -1) | the clicked or selected record |
some / every | boolean | enable a button, validation |
reduce | any single value | totals, 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(' | '));
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
[ 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>
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
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))Remove the row with Id
idfromthis.rowsthe 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.