website/docs/array-methods.md
The Array Methods Plugin (enableArrayMethods()) optimizes array operations within Immer producers by avoiding unnecessary Proxy creation during iteration. This provides significant performance improvements for array-heavy operations.
Why does this matter? Without the plugin, every array element access during iteration (e.g., in filter, find, slice) creates a Proxy object. For a 1000-element array, this means 1000+ proxy trap invocations just to iterate. With the plugin enabled, callbacks receive base (non-proxied) values, and proxies are only created as needed for mutation tracking.
Enable the plugin once at your application's entry point:
import {enableArrayMethods} from "immer"
enableArrayMethods()
This adds approximately 2KB to your bundle size.
These methods modify the array in-place and operate directly on the draft's internal copy without creating per-element proxies:
| Method | Returns | Description |
|---|---|---|
push() | New length | Adds elements to the end |
pop() | Removed element | Removes and returns the last element |
shift() | Removed element | Removes and returns the first element |
unshift() | New length | Adds elements to the beginning |
splice() | Removed elements | Adds/removes elements at any position |
sort() | The draft array | Sorts elements in place |
reverse() | The draft array | Reverses the array in place |
import {produce, enableArrayMethods} from "immer"
enableArrayMethods()
const base = {items: [3, 1, 4, 1, 5]}
const result = produce(base, draft => {
draft.items.push(9) // Adds 9 to end
draft.items.sort() // Sorts: [1, 1, 3, 4, 5, 9]
draft.items.reverse() // Reverses: [9, 5, 4, 3, 1, 1]
})
Non-mutating methods are categorized based on what they return:
These methods select items that exist in the original array and create draft proxies for the returned items. The callbacks receive base values (the optimization), but the returned array contains newly created draft proxies that point back to the original positions. Mutations to returned items WILL affect the draft state.
| Method | Returns | Drafts? |
|---|---|---|
filter() | Array of matching items | ✅ Yes |
slice() | Array of items in range | ✅ Yes |
find() | First matching item or undefined | ✅ Yes |
findLast() | Last matching item or undefined | ✅ Yes |
const base = {
items: [
{id: 1, value: 10},
{id: 2, value: 20},
{id: 3, value: 30}
]
}
const result = produce(base, draft => {
// filter returns drafts - mutations track back to original
const filtered = draft.items.filter(item => item.value > 15)
filtered[0].value = 999 // This WILL affect draft.items[1]
// find returns a draft - mutations track back
const found = draft.items.find(item => item.id === 3)
if (found) {
found.value = 888 // This WILL affect draft.items[2]
}
// slice returns drafts
const sliced = draft.items.slice(0, 2)
sliced[0].value = 777 // This WILL affect draft.items[0]
})
console.log(result.items[0].value) // 777
console.log(result.items[1].value) // 999
console.log(result.items[2].value) // 888
These methods create new arrays that may include external items or restructured data. They return base values, NOT drafts. Mutations to returned items will NOT track back to the draft state.
| Method | Returns | Drafts? |
|---|---|---|
concat() | New combined array | ❌ No |
flat() | New flattened array | ❌ No |
const base = {items: [{id: 1, value: 10}]}
const result = produce(base, draft => {
// concat returns base values - mutations DON'T track
const concatenated = draft.items.concat([{id: 2, value: 20}])
concatenated[0].value = 999 // This will NOT affect draft.items[0]
// To actually use concat results, assign them:
draft.items = draft.items.concat([{id: 2, value: 20}])
})
// Original unchanged because concat result wasn't assigned
console.log(result.items[0].value) // 10 (unchanged)
Why the distinction?
filter, slice, find) select items that exist in the original array. Returning drafts allows mutations to propagate back to the source.concat, flat) create new data structures that may include external items or restructured data, making draft tracking impractical.These methods return primitive values (numbers, booleans, strings). No tracking issues since primitives aren't draftable:
| Method | Returns |
|---|---|
indexOf() | Number (index or -1) |
lastIndexOf() | Number (index or -1) |
includes() | Boolean |
some() | Boolean |
every() | Boolean |
findIndex() | Number (index or -1) |
findLastIndex() | Number (index or -1) |
join() | String |
toString() | String |
toLocaleString() | String |
const base = {
items: [
{id: 1, active: true},
{id: 2, active: false}
]
}
const result = produce(base, draft => {
const index = draft.items.findIndex(item => item.id === 2)
const hasActive = draft.items.some(item => item.active)
const allActive = draft.items.every(item => item.active)
console.log(index) // 1
console.log(hasActive) // true
console.log(allActive) // false
})
The following methods are not intercepted by the plugin and work through standard Proxy behavior. Callbacks receive drafts, and mutations track normally:
| Method | Description |
|---|---|
map() | Transform each element |
flatMap() | Map then flatten |
forEach() | Execute callback for each element |
reduce() | Reduce to single value |
reduceRight() | Reduce from right to left |
const base = {
items: [
{id: 1, value: 10, nested: {count: 0}},
{id: 2, value: 20, nested: {count: 0}}
]
}
const result = produce(base, draft => {
// forEach receives drafts - mutations work normally
draft.items.forEach(item => {
item.value *= 2
})
// map is NOT overridden - callbacks receive drafts
// The returned array items are also drafts (extracted from draft.items)
const mapped = draft.items.map(item => item.nested)
// Mutations to the result array propagate back
mapped[0].count = 999 // ✅ This affects draft.items[0].nested.count
})
console.log(result.items[0].nested.count) // 999
For overridden methods, callbacks receive base values (not drafts). This is the core optimization - it avoids creating proxies for every element during iteration.
const base = {
items: [
{id: 1, value: 10},
{id: 2, value: 20}
]
}
produce(base, draft => {
draft.items.filter(item => {
// `item` is a base value here, NOT a draft
// Reading properties works fine
return item.value > 15
// But direct mutation here won't be tracked:
// item.value = 999 // ❌ Won't affect draft
})
// Instead, use the returned draft:
const filtered = draft.items.filter(item => item.value > 15)
filtered[0].value = 999 // ✅ This works because filtered[0] is a draft
})
| Category | Methods | Returns | Mutations Track? |
|---|---|---|---|
| Subset | filter, slice, find, findLast | Draft proxies | ✅ Yes |
| Transform | concat, flat | Base values | ❌ No |
| Primitive | indexOf, includes, some, every, findIndex, findLastIndex, lastIndexOf, join, toString, toLocaleString | Primitives | N/A |
| Mutating | push, pop, shift, unshift, splice, sort, reverse | Various | ✅ Yes (modifies draft) |
| Not Overridden | map, flatMap, forEach, reduce, reduceRight | Standard behavior | ✅ Yes (callbacks get drafts) |
Enable the Array Methods Plugin when:
filter, find, some, every on large arraysThe plugin is most beneficial for:
Without the plugin:
filter() on 1000 elements = 1000+ proxy creationsWith the plugin:
// Without plugin: ~3000+ proxy trap invocations
// With plugin: ~10-20 proxy trap invocations
const result = produce(largeState, draft => {
const filtered = draft.items.filter(x => x.value > threshold)
// Only items you mutate get proxied
filtered.forEach(item => {
item.processed = true
})
})