Back to stories
<Frontend/>

Map vs Array.findIndex in JavaScript – When to Use Each

Share by

Map vs Array.findIndex in JavaScript – When to Use Each

When you have a list of items and need to find one by id over and over—in a hot path, a render loop, or a large dataset—the choice between scanning the array (findIndex / find) and looking up by key (Map or object) has a big impact. This post explains both approaches, their complexity, and when to use each, with runnable examples.

The same comparison is available as an interactive demo with a live benchmark — use the Try demo link in the header above.


What we're comparing

Array + findIndex: You keep an array of objects. To get an item by id you do arr.findIndex(item => item.id === id) (or find). Each lookup walks the array until it finds a match — O(n) per lookup.

Map (or object): You keep a Map (or plain object) from id → item. To get an item by id you do map.get(id) or obj[id]. Each lookup is a single key access — O(1) average.

Same “logical” data: a collection of items identifiable by id. Different structures, different lookup cost.


Example: a list of users

We'll use a small user list in both shapes so you can compare.

Array + findIndex

type User = { id: string; name: string; role: string }

const usersArray: User[] = [
  { id: '1', name: 'Alice', role: 'admin' },
  { id: '2', name: 'Bob', role: 'user' },
  { id: '3', name: 'Carol', role: 'editor' },
]

Get one user by id:

function getUserByIndex(users: User[], id: string): User | undefined {
  const index = users.findIndex((u) => u.id === id)
  return index === -1 ? undefined : users[index]
}

Or with find:

function getUserFind(users: User[], id: string): User | undefined {
  return users.find((u) => u.id === id)
}

Both scan the array; average cost grows with length (O(n)).


Map by id

type User = { id: string; name: string; role: string }

// Build once from array (e.g. from API)
const usersArray: User[] = [
  { id: '1', name: 'Alice', role: 'admin' },
  { id: '2', name: 'Bob', role: 'user' },
  { id: '3', name: 'Carol', role: 'editor' },
]

const usersById = new Map<string, User>(
  usersArray.map((u) => [u.id, u])
)

Get one user by id:

function getUserByMap(map: Map<string, User>, id: string): User | undefined {
  return map.get(id)
}

Lookup is O(1) on average. You do pay an upfront cost to build the Map; if the list never changes, that’s a one-time O(n).


Realtime example: lookup in a loop

Imagine you’re rendering a list of “comments” and for each comment you need the author (user) by authorId. If you only have an array of users:

// O(comments.length * users.length) — can get slow
comments.forEach((comment) => {
  const author = users.find((u) => u.id === comment.authorId)
  renderComment(comment, author)
})

If you have a Map:

// O(comments.length) — each author lookup is O(1)
comments.forEach((comment) => {
  const author = usersById.get(comment.authorId)
  renderComment(comment, author)
})

Same UI, different cost. For dozens of comments and users it might not matter; for hundreds or thousands, Map wins.


Building and maintaining the Map

One-time build (read-only list):

const usersById = new Map(usersArray.map((u) => [u.id, u]))

When the list can change: you must keep the Map in sync. Add/update/delete in both the array (if you still need order) and the Map.

function addUser(
  array: User[],
  map: Map<string, User>,
  user: User
): void {
  array.push(user)
  map.set(user.id, user)
}

function updateUser(
  array: User[],
  map: Map<string, User>,
  id: string,
  patch: Partial<User>
): void {
  const index = array.findIndex((u) => u.id === id)
  if (index === -1) return
  const updated = { ...array[index], ...patch }
  array[index] = updated
  map.set(id, updated)
}

function removeUser(
  array: User[],
  map: Map<string, User>,
  id: string
): void {
  const index = array.findIndex((u) => u.id === id)
  if (index === -1) return
  array.splice(index, 1)
  map.delete(id)
}

So: Map gives O(1) lookup but you pay with extra structure and sync logic. For a static or rarely-changing list, build the Map once and enjoy O(1) lookups.


Map vs plain object (Record<string, User>)

For string (or number) keys, a plain object works too:

const usersById: Record<string, User> = {}
usersArray.forEach((u) => { usersById[u.id] = u })
// lookup: usersById[id]
  • Map: Better when keys might not be strings, you need insertion order, or you want to avoid prototype pollution. Slightly more verbose (map.get(id) vs obj[id]).
  • Object: Slightly less code and very fast for string keys. Use when keys are plain strings and you don’t need Map’s extras.

Complexity is the same: O(1) lookup. The benchmark demo uses Map; in practice object lookup is comparable.


Pros and cons

Array + findIndex — pros

  • Single source of structure: Only one collection; no duplicate data or sync logic.
  • Order is explicit: The array order is the list order; easy to sort, filter, slice.
  • Simple for small lists: For tens of items and few lookups, code is straightforward and performance is fine.
  • Fits API responses: Many APIs return [{ id, ... }, ...]; you can use that array as-is.

Array + findIndex — cons

  • Lookup is O(n): Every find/findIndex can scan the whole array. Cost grows with list size and lookup count.
  • Hot paths hurt: If you lookup by id inside a render loop or in a tight loop, the cost multiplies (e.g. comments × users).
  • Update/delete by id also O(n): You need the index first, so you still pay a scan (or maintain a separate index).

Map (or object) — pros

  • O(1) lookup by id: Constant-time access; scales to large lists and many lookups.
  • Predictable cost: Lookup time doesn’t depend on list size.
  • Good for “get by id” hot paths: Rendering, event handlers, merging by id, etc.

Map (or object) — cons

  • Extra structure: You store both the array (if you need order) and the Map; more memory and code.
  • Must keep in sync: Add/update/delete must touch both; bugs can leave them out of sync.
  • Overkill for tiny lists: For a handful of items and rare lookups, the benefit is small.

When to use which

  • Prefer array + findIndex when:
    • The list is small (e.g. < 50 items) and you don’t lookup by id often.
    • You only need the list in order and never “get by id” in a hot path.
    • You want minimal code and no extra structure.
  • Prefer Map (or object) when:
    • You need “get by id” often or in a loop (e.g. render comments with authors).
    • The list is large or the number of lookups is high.
    • You already have normalized state (e.g. entities by id) — then you’re already doing O(1) lookup.

You can mix: keep an array for “list in order” and a Map for “get by id”. Build the Map once from the array when data loads; if the list is mutable, update both on add/update/delete.


Summary

  • Array + findIndex: Simple, one structure, O(n) per lookup. Good for small lists and rare lookups.
  • Map (or object): O(1) lookup by id, better for large lists and hot paths. Requires building and optionally maintaining a second structure.
  • Use the demo (link above) to run a live benchmark and see the difference at different list sizes and lookup counts.