Stop Looping with find(): When Map Actually Earns Its Place in Frontend Code
Foreword
Many frontend developers know about Map, but rarely use it when writing actual business logic.
The reason is simple: data returned by APIs is usually an array, form submission data is usually an object or array, and what's finally sent to the backend is JSON. Most of the time, arrays and plain objects are sufficient.
But there's one type of problem where Map becomes extremely handy as soon as it appears:
Quickly finding corresponding data based on a unique key.
In other words, the core value of Map is not "storing data," but organizing a set of data into an index table.
This article uses three scenarios to illustrate:
- When an array is more appropriate.
- When you can use
Mapto manage selected items. - How to use
Mapto preserve user edits after an API refresh.
The example code uses Vue 3, but the core ideas are not dependent on Vue.
First, a Judgement Criterion
When you write code like this, you can consider whether you should use Map:
listA.map(itemA => {
const itemB = listB.find(itemB => itemB.id === itemA.id)
})
This code itself is not wrong.
But the signal behind it is: you are looking up data from another list based on id.
If this lookup happens only once or twice, find is very intuitive. But if it appears inside a loop and the data volume is large, it becomes repeated scanning:
listA.length === 1000
listB.length === 1000
In the worst case, every item in listA needs to search through listB, which is:
1000 * 1000
At this point, you can first build an index for listB:
const listBMap = new Map(listB.map(itemB => [itemB.id, itemB]))
listA.map(itemA => {
const itemB = listBMap.get(itemA.id)
})
It's divided into two steps:
- First organize the array into a
Map. - Then directly get data by
key.
Original array:
const listB = [
{ id: 1, name: 'A' },
{ id: 2, name: 'B' },
{ id: 3, name: 'C' }
]
After organizing into a Map, it can be understood as:
1 => { id: 1, name: 'A' }
2 => { id: 2, name: 'B' }
3 => { id: 3, name: 'C' }
The core difference is:
find: searches again every time.Map: builds the index in advance, then looks up directly.
Scenario 1: Simple Checkbox, an Array is Enough
Let's look at the most common scenario: product multi-selection.
There are three products on the page, and the user can check, select all, and clear. If you only need to submit the selected product IDs in the end, then an array is the lightest solution.
const goods = [
{ id: 101, name: 'Keyboard', price: 299 },
{ id: 102, name: 'Mouse', price: 189 },
{ id: 103, name: 'Monitor Stand', price: 129 }
]
In Vue, you can write it directly like this:
<template>
<section>
<p>Selected {{ selectedIds.length }} items</p>
<button type="button" @click="selectAll">Select All</button>
<button type="button" @click="clearSelected">Clear</button>
<label v-for="item in goods" :key="item.id">
<input v-model="selectedIds" type="checkbox" :value="item.id" />
{{ item.name }}
</label>
</section>
</template>
<script setup>
import { ref } from 'vue'
const goods = [
{ id: 101, name: 'Keyboard', price: 299 },
{ id: 102, name: 'Mouse', price: 189 },
{ id: 103, name: 'Monitor Stand', price: 129 }
]
const selectedIds = ref([])
const selectAll = () => {
selectedIds.value = goods.map(item => item.id)
}
const clearSelected = () => {
selectedIds.value = []
}
</script>
What you get in the end here is:
[101, 102, 103]
This structure is very suitable for submitting to an API.
So the first conclusion is:
For simple form values, don't use
Mapjust for the sake of usingMap. An array is simpler and more semantically aligned with forms.
Scenario 2: When Selected Items Need the Full Object, Consider Map
Still product multi-selection.
If the business not only cares about product IDs but also frequently needs to directly get the selected product objects—for example, displaying selected product details, calculating the total price, or performing partial deletion—then Map becomes more natural.
The structure can be designed as:
Product ID -> Product Object
Corresponding code:
<template>
<section>
<p>Selected {{ selectedMap.size }} items</p>
<button type="button" @click="selectAll">Select All</button>
<button type="button" @click="clearSelected">Clear</button>
<label v-for="item in goods" :key="item.id">
<input type="checkbox" :checked="selectedMap.has(item.id)" @change="toggleItem(item)" />
{{ item.name }}
</label>
</section>
</template>
<script setup>
import { reactive } from 'vue'
const goods = [
{ id: 101, name: 'Keyboard', price: 299 },
{ id: 102, name: 'Mouse', price: 189 },
{ id: 103, name: 'Monitor Stand', price: 129 }
]
const selectedMap = reactive(new Map())
const toggleItem = item => {
if (selectedMap.has(item.id)) {
selectedMap.delete(item.id)
return
}
selectedMap.set(item.id, item)
}
const selectAll = () => {
goods.forEach(item => {
selectedMap.set(item.id, item)
})
}
const clearSelected = () => {
selectedMap.clear()
}
</script>
This code reads very close to the business logic:
has: Is this product selected?set: Select this product.delete: Deselect this product.clear: Clear all selections.size: How many items are currently selected?
If you later need to get a specific selected product, you don't need to go back to the original array and find:
selectedMap.get(101)
However, this scenario still doesn't mean "product multi-selection must use Map."
The real criterion is:
If you only need an array of IDs, use an array.
If you frequently need to check, delete, or read the full object by ID, considerMap.
Scenario 3: Preserving User Edits After an API Refresh
This scenario is closer to real business logic.
Suppose there is a batch collection table:
- The API returns a batch of orders.
- The user fills in "Current Collection" and "Remarks."
- The page refreshes the API data; order status and receivable amount may change.
- But the content the user just filled in must not be lost.
Two types of data exist simultaneously here:
- API data: Order ID, Customer, Status, Receivable Amount
- User edits: Current Collection, Remarks
The goal is also very clear:
- Order ID, Customer, Status, Receivable Amount should be based on the latest API data.
- Current Collection and Remarks should be based on the user's current edits.
- The table order should follow the order returned by the API.
First, prepare two versions of API data to simulate a refresh:
const apiOrderVersions = [
[
{
orderId: '1001',
customerName: 'Zhang San',
orderStatus: 'Pending Collection',
receivableAmount: 200,
receivedAmount: 0
},
{
orderId: '1002',
customerName: 'Li Si',
orderStatus: 'Pending Collection',
receivableAmount: 350,
receivedAmount: 0
}
],
[
{
orderId: '1001',
customerName: 'Zhang San',
orderStatus: 'Partial Collection',
receivableAmount: 210,
receivedAmount: 0
},
{
orderId: '1002',
customerName: 'Li Si',
orderStatus: 'Pending Collection',
receivableAmount: 350,
receivedAmount: 0
},
{
orderId: '1003',
customerName: 'Wang Wu',
orderStatus: 'New Order',
receivableAmount: 480,
receivedAmount: 0
}
]
]
User edits are saved using Map:
const selectedOrderMap = reactive(new Map())
It represents: Order ID -> User Edit Fields
For example:
'1001' => {
receivedAmount: 120,
remark: 'Collected offline'
}
Why Order Refresh is Suitable for Map
The key issue is:
Every API order needs to find its corresponding user edit record.
If you don't use Map, it's easy to write:
const selectedOrder = selectedOrders.find(order => order.orderId === row.orderId)
When this logic appears inside a map, it becomes:
orderRows.map(row => {
const selectedOrder = selectedOrders.find(order => order.orderId === row.orderId)
})
That is, for every row of API data processed, the user edit list must be scanned once.
After using Map, when saving the edit state, write directly by order ID:
const updateOrder = row => {
row.remainingAmount = row.receivableAmount - row.receivedAmount
selectedOrderMap.set(row.orderId, {
receivedAmount: row.receivedAmount,
remark: row.remark
})
}
There's no need to judge whether it's an addition or replacement here:
- The first time you edit an order,
setwill add it. - Editing the same order again,
setwill overwrite it. - The same
orderIdwill only have one edit record in theMap.
Merging API Data and User Edits
When generating table rows, base it on the API data, then merge user edits:
const createOrderRows = () => {
return apiOrderVersions[apiVersionIndex.value].map(row => {
const mergedRow = {
...row,
...selectedOrderMap.get(row.orderId)
}
return {
...mergedRow,
remainingAmount: mergedRow.receivableAmount - mergedRow.receivedAmount
}
})
}
The business model corresponding to this code is:
API Order Row -> Read User Edit by Order ID -> Generate Editable Table Row
Note the spread order here:
{
...row,
...selectedOrderMap.get(row.orderId)
}
Spread the API row first, then the user edits.
This way, the receivedAmount and remark the user just filled in can overwrite the API defaults.
At the same time, the table row is a new object generated through object spreading:
const mergedRow = {
...row,
...selectedOrderMap.get(row.orderId)
}
For this kind of flat order object, this can prevent v-model from directly modifying the original API data, eliminating the need to write JSON.parse(JSON.stringify(...)).
Don't Save the Entire Order Row in Map
There's a very important detail here.
When saving user edits, it's recommended to only save the fields the user actually modifies:
selectedOrderMap.set(row.orderId, {
receivedAmount: row.receivedAmount,
remark: row.remark
})
Don't write:
selectedOrderMap.set(row.orderId, {
...row,
receivedAmount: row.receivedAmount,
remark: row.remark
})
Because ...row will also store API fields, such as:
customerNameorderStatusreceivableAmountremainingAmount
After refreshing the API, if you merge like this again:
{
...row,
...selectedOrderMap.get(row.orderId)
}
The old entire order row might overwrite the new status and new receivable amount just returned by the API.
So in this case, you need to distinguish between two types of fields:
| Field Source | Field Examples | Handling Method |
|---|---|---|
| API Data | customerName, orderStatus, receivableAmount |
Always use the API as the source on each refresh |
| User Edits | receivedAmount, remark |
Store in Map, merge after refresh |
This is safer than "saving the entire row and then overwriting the entire row."
Core Code
After removing styles and table details, the core code is actually not much:
<script setup>
import { computed, reactive, ref } from 'vue'
const apiVersionIndex = ref(0)
const selectedOrderMap = reactive(new Map())
const apiVersionLabel = computed(() => `${apiVersionIndex.value + 1} / ${apiOrderVersions.length}`)
const createOrderRows = () => {
return apiOrderVersions[apiVersionIndex.value].map(row => {
const mergedRow = {
...row,
...selectedOrderMap.get(row.orderId)
}
return {
...mergedRow,
remainingAmount: mergedRow.receivableAmount - mergedRow.receivedAmount
}
})
}
const orderRows = ref(createOrderRows())
const updateOrder = row => {
row.remainingAmount = row.receivableAmount - row.receivedAmount
selectedOrderMap.set(row.orderId, {
receivedAmount: row.receivedAmount,
remark: row.remark
})
}
const refreshOrders = () => {
apiVersionIndex.value = (apiVersionIndex.value + 1) % apiOrderVersions.length
orderRows.value = createOrderRows()
}
const resetDemo = () => {
apiVersionIndex.value = 0
selectedOrderMap.clear()
orderRows.value = createOrderRows()
}
</script>
When refreshing, selectedOrderMap is not cleared, so user input remains.
When resetting, selectedOrderMap is cleared, and the page returns to the first version of the API data.
Looking at the Three Scenarios Together
| Scenario | Data Structure | Suitable Reason |
|---|---|---|
| Simple checkbox form value | id[] |
Lightweight, suitable for direct API submission. |
| Selected items need full object | Map<id, item> |
Convenient for checking, deleting, and reading objects by ID. |
| Preserve user edits after API refresh | Map<orderId, editData> |
Keep temporary edits by business key, then merge back into the latest API data. |
When You Should Think of Map
When you frequently write this kind of code, consider Map:
arrayA.map(itemA => {
arrayB.find(itemB => itemB.id === itemA.id)
})
selectedList.some(item => item.id === id)
selectedList.filter(item => item.id !== id)
This doesn't mean these patterns must all be replaced.
They just remind you: there might be a "key-to-data" mapping relationship here.
If this mapping relationship is only written temporarily once or twice, using an array is fine.
If this mapping relationship is already core to the business, Map is worth bringing on stage.
Final Summary
Map is not high-frequency in frontend business logic, because a lot of data ultimately needs to go back to JSON.
But when the problem becomes "finding data by key," it's very useful:
- List selection state needs to save the full object.
- Need to quickly check if a piece of data exists by ID.
- Two lists need to be merged by a business key.
- Need to preserve user temporary edits after an API refresh.
- Don't want to repeatedly
findinside a loop.
Simple forms use arrays, fixed configurations use objects.
But as soon as you start doing lookups, merges, and overwrites around a certain unique key, Map is a tool well worth considering.