跪拜 Guibai
← Back to the summary

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:

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:

  1. First organize the array into a Map.
  2. 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.

image.png

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 Map just for the sake of using Map. 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:

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, consider Map.

Scenario 3: Preserving User Edits After an API Refresh

This scenario is closer to real business logic.

image.png Suppose there is a batch collection table:

  1. The API returns a batch of orders.
  2. The user fills in "Current Collection" and "Remarks."
  3. The page refreshes the API data; order status and receivable amount may change.
  4. But the content the user just filled in must not be lost.

Two types of data exist simultaneously here:

  1. API data: Order ID, Customer, Status, Receivable Amount
  2. User edits: Current Collection, Remarks

The goal is also very clear:

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:

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:

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:

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.