Skip to content

Commit ed5347c

Browse files
committed
Add array plugin docs
1 parent 77683ed commit ed5347c

File tree

4 files changed

+315
-0
lines changed

4 files changed

+315
-0
lines changed

website/docs/api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ title: API overview
1616
| `createDraft` | Given a base state, creates a mutable draft for which any modifications will be recorded | [Async](./async.mdx) |
1717
| `current` | Given a draft object (doesn't have to be a tree root), takes a snapshot of the current state of the draft | [Current](./current.md) |
1818
| `Draft<T>` | Exposed TypeScript type to convert an immutable type to a mutable type | [TypeScript](./typescript.mdx) |
19+
| `enableArrayMethods()` | Enables optimized array method handling for improved performance with array-heavy operations. | [Array Methods](./array-methods.md) |
1920
| `enableMapSet()` | Enables support for `Map` and `Set` collections. | [Installation](./installation.mdx#pick-your-immer-version) |
2021
| `enablePatches()` | Enables support for JSON patches. | [Installation](./installation#pick-your-immer-version) |
2122
| `finishDraft` | Given an draft created using `createDraft`, seals the draft and produces and returns the next immutable state that captures all the changes | [Async](./async.mdx) |

website/docs/array-methods.md

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
---
2+
id: array-methods
3+
title: Array Methods Plugin
4+
---
5+
6+
<center>
7+
<div data-ea-publisher="immerjs" data-ea-type="image" className="horizontal bordered"></div>
8+
</center>
9+
10+
## Overview
11+
12+
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.
13+
14+
**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.
15+
16+
## Installation
17+
18+
Enable the plugin once at your application's entry point:
19+
20+
```javascript
21+
import {enableArrayMethods} from "immer"
22+
23+
enableArrayMethods()
24+
```
25+
26+
This adds approximately **2KB** to your bundle size.
27+
28+
## Mutating Methods
29+
30+
These methods modify the array in-place and operate directly on the draft's internal copy without creating per-element proxies:
31+
32+
| Method | Returns | Description |
33+
| ----------- | ---------------- | ------------------------------------- |
34+
| `push()` | New length | Adds elements to the end |
35+
| `pop()` | Removed element | Removes and returns the last element |
36+
| `shift()` | Removed element | Removes and returns the first element |
37+
| `unshift()` | New length | Adds elements to the beginning |
38+
| `splice()` | Removed elements | Adds/removes elements at any position |
39+
| `sort()` | The draft array | Sorts elements in place |
40+
| `reverse()` | The draft array | Reverses the array in place |
41+
42+
```javascript
43+
import {produce, enableArrayMethods} from "immer"
44+
45+
enableArrayMethods()
46+
47+
const base = {items: [3, 1, 4, 1, 5]}
48+
49+
const result = produce(base, draft => {
50+
draft.items.push(9) // Adds 9 to end
51+
draft.items.sort() // Sorts: [1, 1, 3, 4, 5, 9]
52+
draft.items.reverse() // Reverses: [9, 5, 4, 3, 1, 1]
53+
})
54+
```
55+
56+
## Non-Mutating Methods
57+
58+
Non-mutating methods are categorized based on what they return:
59+
60+
### Subset Operations (Return Drafts)
61+
62+
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.**
63+
64+
| Method | Returns | Drafts? |
65+
| ------------ | ---------------------------------- | ------- |
66+
| `filter()` | Array of matching items | ✅ Yes |
67+
| `slice()` | Array of items in range | ✅ Yes |
68+
| `find()` | First matching item or `undefined` | ✅ Yes |
69+
| `findLast()` | Last matching item or `undefined` | ✅ Yes |
70+
71+
```javascript
72+
const base = {
73+
items: [
74+
{id: 1, value: 10},
75+
{id: 2, value: 20},
76+
{id: 3, value: 30}
77+
]
78+
}
79+
80+
const result = produce(base, draft => {
81+
// filter returns drafts - mutations track back to original
82+
const filtered = draft.items.filter(item => item.value > 15)
83+
filtered[0].value = 999 // This WILL affect draft.items[1]
84+
85+
// find returns a draft - mutations track back
86+
const found = draft.items.find(item => item.id === 3)
87+
if (found) {
88+
found.value = 888 // This WILL affect draft.items[2]
89+
}
90+
91+
// slice returns drafts
92+
const sliced = draft.items.slice(0, 2)
93+
sliced[0].value = 777 // This WILL affect draft.items[0]
94+
})
95+
96+
console.log(result.items[0].value) // 777
97+
console.log(result.items[1].value) // 999
98+
console.log(result.items[2].value) // 888
99+
```
100+
101+
### Transform Operations (Return Base Values)
102+
103+
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.**
104+
105+
| Method | Returns | Drafts? |
106+
| ---------- | ------------------- | ------- |
107+
| `concat()` | New combined array | ❌ No |
108+
| `flat()` | New flattened array | ❌ No |
109+
110+
```javascript
111+
const base = {items: [{id: 1, value: 10}]}
112+
113+
const result = produce(base, draft => {
114+
// concat returns base values - mutations DON'T track
115+
const concatenated = draft.items.concat([{id: 2, value: 20}])
116+
concatenated[0].value = 999 // This will NOT affect draft.items[0]
117+
118+
// To actually use concat results, assign them:
119+
draft.items = draft.items.concat([{id: 2, value: 20}])
120+
})
121+
122+
// Original unchanged because concat result wasn't assigned
123+
console.log(result.items[0].value) // 10 (unchanged)
124+
```
125+
126+
**Why the distinction?**
127+
128+
- **Subset operations** (`filter`, `slice`, `find`) select items that exist in the original array. Returning drafts allows mutations to propagate back to the source.
129+
- **Transform operations** (`concat`, `flat`) create new data structures that may include external items or restructured data, making draft tracking impractical.
130+
131+
### Primitive-Returning Methods
132+
133+
These methods return primitive values (numbers, booleans, strings). No tracking issues since primitives aren't draftable:
134+
135+
| Method | Returns |
136+
| ------------------ | -------------------- |
137+
| `indexOf()` | Number (index or -1) |
138+
| `lastIndexOf()` | Number (index or -1) |
139+
| `includes()` | Boolean |
140+
| `some()` | Boolean |
141+
| `every()` | Boolean |
142+
| `findIndex()` | Number (index or -1) |
143+
| `findLastIndex()` | Number (index or -1) |
144+
| `join()` | String |
145+
| `toString()` | String |
146+
| `toLocaleString()` | String |
147+
148+
```javascript
149+
const base = {
150+
items: [
151+
{id: 1, active: true},
152+
{id: 2, active: false}
153+
]
154+
}
155+
156+
const result = produce(base, draft => {
157+
const index = draft.items.findIndex(item => item.id === 2)
158+
const hasActive = draft.items.some(item => item.active)
159+
const allActive = draft.items.every(item => item.active)
160+
161+
console.log(index) // 1
162+
console.log(hasActive) // true
163+
console.log(allActive) // false
164+
})
165+
```
166+
167+
## Methods NOT Overridden
168+
169+
The following methods are **not** intercepted by the plugin and work through standard Proxy behavior. Callbacks receive drafts, and mutations track normally:
170+
171+
| Method | Description |
172+
| --------------- | --------------------------------- |
173+
| `map()` | Transform each element |
174+
| `flatMap()` | Map then flatten |
175+
| `forEach()` | Execute callback for each element |
176+
| `reduce()` | Reduce to single value |
177+
| `reduceRight()` | Reduce from right to left |
178+
179+
```javascript
180+
const base = {
181+
items: [
182+
{id: 1, value: 10, nested: {count: 0}},
183+
{id: 2, value: 20, nested: {count: 0}}
184+
]
185+
}
186+
187+
const result = produce(base, draft => {
188+
// forEach receives drafts - mutations work normally
189+
draft.items.forEach(item => {
190+
item.value *= 2
191+
})
192+
193+
// map is NOT overridden - callbacks receive drafts
194+
// The returned array items are also drafts (extracted from draft.items)
195+
const mapped = draft.items.map(item => item.nested)
196+
// Mutations to the result array propagate back
197+
mapped[0].count = 999 // ✅ This affects draft.items[0].nested.count
198+
})
199+
200+
console.log(result.items[0].nested.count) // 999
201+
```
202+
203+
## Callback Behavior
204+
205+
For overridden methods, callbacks receive **base values** (not drafts). This is the core optimization - it avoids creating proxies for every element during iteration.
206+
207+
```javascript
208+
const base = {
209+
items: [
210+
{id: 1, value: 10},
211+
{id: 2, value: 20}
212+
]
213+
}
214+
215+
produce(base, draft => {
216+
draft.items.filter(item => {
217+
// `item` is a base value here, NOT a draft
218+
// Reading properties works fine
219+
return item.value > 15
220+
221+
// But direct mutation here won't be tracked:
222+
// item.value = 999 // ❌ Won't affect draft
223+
})
224+
225+
// Instead, use the returned draft:
226+
const filtered = draft.items.filter(item => item.value > 15)
227+
filtered[0].value = 999 // ✅ This works because filtered[0] is a draft
228+
})
229+
```
230+
231+
## Method Return Behavior Summary
232+
233+
| Category | Methods | Returns | Mutations Track? |
234+
| --- | --- | --- | --- |
235+
| **Subset** | `filter`, `slice`, `find`, `findLast` | Draft proxies | ✅ Yes |
236+
| **Transform** | `concat`, `flat` | Base values | ❌ No |
237+
| **Primitive** | `indexOf`, `includes`, `some`, `every`, `findIndex`, `findLastIndex`, `lastIndexOf`, `join`, `toString`, `toLocaleString` | Primitives | N/A |
238+
| **Mutating** | `push`, `pop`, `shift`, `unshift`, `splice`, `sort`, `reverse` | Various | ✅ Yes (modifies draft) |
239+
| **Not Overridden** | `map`, `flatMap`, `forEach`, `reduce`, `reduceRight` | Standard behavior | ✅ Yes (callbacks get drafts) |
240+
241+
## When to Use
242+
243+
Enable the Array Methods Plugin when:
244+
245+
- Your application has significant array iteration within producers
246+
- You frequently use methods like `filter`, `find`, `some`, `every` on large arrays
247+
- Performance profiling shows array operations as a bottleneck
248+
249+
The plugin is most beneficial for:
250+
251+
- Large arrays (100+ elements)
252+
- Frequent producer calls with array operations
253+
- Read-heavy operations (filtering, searching) where most elements aren't modified
254+
255+
## Performance Benefit
256+
257+
**Without the plugin:**
258+
259+
- Every array element access during iteration creates a Proxy
260+
- A `filter()` on 1000 elements = 1000+ proxy creations
261+
262+
**With the plugin:**
263+
264+
- Callbacks receive base values directly
265+
- Proxies only created for the specific elements you actually mutate, or that match filtering predicates
266+
267+
```javascript
268+
// Without plugin: ~3000+ proxy trap invocations
269+
// With plugin: ~10-20 proxy trap invocations
270+
const result = produce(largeState, draft => {
271+
const filtered = draft.items.filter(x => x.value > threshold)
272+
// Only items you mutate get proxied
273+
filtered.forEach(item => {
274+
item.processed = true
275+
})
276+
})
277+
```

website/docs/performance.mdx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,17 @@ Most important observation:
6969

7070
## Performance tips
7171

72+
### Enable the Array Methods Plugin
73+
74+
For applications with significant array iteration within producers, enable the [Array Methods Plugin](./array-methods.md):
75+
76+
```javascript
77+
import {enableArrayMethods} from "immer"
78+
enableArrayMethods()
79+
```
80+
81+
This plugin optimizes array operations like `filter`, `find`, `some`, `every`, and `slice` by avoiding proxy creation for every element during iteration. Without the plugin, iterating a 1000-element array creates 1000+ proxies. With the plugin, callbacks receive base values, and proxies are only created for elements you actually mutate.
82+
7283
### Pre-freeze data
7384

7485
When adding a large data set to the state tree in an Immer producer (for example data received from a JSON endpoint), it is worth to call `freeze(json)` on the root of the data that is being added first. To _shallowly_ freeze it. This will allow Immer to add the new data to the tree faster, as it will avoid the need to _recursively_ scan and freeze the new data.

website/docs/pitfalls.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,29 @@ remove(values, a)
8787
```
8888

8989
If possible, it's recommended to perform the comparison outside the `produce` function, or to use a unique identifier property like `.id` instead, to avoid needing to use `original`.
90+
91+
### Array Methods Plugin: Callbacks receive base values
92+
93+
When using the [Array Methods Plugin](./array-methods.md) (`enableArrayMethods()`), callbacks for overridden methods like `filter`, `find`, `some`, `every`, and `slice` receive **base values** (not drafts). This is the core performance optimization - it avoids creating proxies for every element during iteration.
94+
95+
```javascript
96+
import {enableArrayMethods, produce} from "immer"
97+
enableArrayMethods()
98+
99+
produce(state, draft => {
100+
draft.items.filter(item => {
101+
// `item` is a base value here, NOT a draft
102+
// Reading works fine:
103+
return item.value > 10
104+
105+
// But direct mutation here won't be tracked:
106+
// item.value = 999 // ❌ Won't affect the draft!
107+
})
108+
109+
// Instead, use the returned result (which contains drafts):
110+
const filtered = draft.items.filter(item => item.value > 10)
111+
filtered[0].value = 999 // ✅ This works - filtered[0] is a draft
112+
})
113+
```
114+
115+
This only applies to methods intercepted by the plugin. Methods like `map`, `forEach`, `reduce` are NOT overridden and work normally - their callbacks receive drafts.

0 commit comments

Comments
 (0)