• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# ForEach: Rendering of Repeated Content
2
3
4**ForEach** enables repeated content based on array-type data.
5
6> **NOTE**
7>
8> Since API version 9, this API is supported in ArkTS widgets.
9
10## API Description
11
12
13```ts
14ForEach(
15  arr: Array,
16  itemGenerator: (item: Array, index?: number) => void,
17  keyGenerator?: (item: Array, index?: number): string => string
18)
19```
20
21
22| Name          | Type                                    | Mandatory  | Description                                    |
23| ------------- | ---------------------------------------- | ---- | ---------------------------------------- |
24| arr           | Array                                    | Yes   | An array, which can be empty, in which case no child component is created. The functions that return array-type values are also allowed, for example, **arr.slice (1, 3)**. The set functions cannot change any state variables including the array itself, such as **Array.splice**, **Array.sort**, and **Array.reverse**.|
25| itemGenerator | (item: any, index?: number) =&gt; void | Yes   | A lambda function used to generate one or more child components for each data item in an array. Each component and its child component list must be contained in parentheses.<br>**NOTE**<br>- The type of the child component must be the one allowed inside the parent container component of **ForEach**. For example, a **\<ListItem>** child component is allowed only when the parent container component of **ForEach** is **\<List>**.<br>- The child build function is allowed to return an **if** or another **ForEach**. **ForEach** can be placed inside **if**.<br>- The optional **index** parameter should only be specified in the function signature if used in its body.|
26| keyGenerator  | (item: any, index?: number) =&gt; string | No   | An anonymous function used to generate a unique and fixed key value for each data item in an array. This key-value generator is optional. However, for performance reasons, it is strongly recommended that the key-value generator be provided, so that the development framework can better identify array changes. For example, if no key-value generator is provided, a reverse of an array will result in rebuilding of all nodes in **ForEach**.<br>**NOTE**<br>- Two items inside the same array must never work out the same ID.<br>- If **index** is not used, an item's ID must not change when the item's position within the array changes. However, if **index** is used, then the ID must change when the item is moved within the array.<br>- When an item is replaced by a new one (with a different value), the ID of the replaced and the ID of the new item must be different.<br>- When **index** is used in the build function, it should also be used in the ID generation function.<br>- The ID generation function is not allowed to mutate any component state.|
27
28
29## Restrictions
30
31- **ForEach** must be used in container components.
32
33- The type of the child component must be the one allowed inside the parent container component of **ForEach**.
34
35- The **itemGenerator** function can contain an **if/else** statement, and an **if/else** statement can contain **ForEach**.
36
37- The call sequence of **itemGenerator** functions may be different from that of the data items in the array. During the development, do not assume whether or when the **itemGenerator** and **keyGenerator** functions are executed. For example, the following example may not run properly:
38
39    ```ts
40    let obj: Object
41    ForEach(anArray.map((item1: Object, index1: number): Object => {
42        obj.i = index1 + 1
43        obj.data = item1
44        return obj;
45      }),
46    (item: string) => Text(`${item.i}. item.data.label`),
47    (item: string): string => {
48        return item.data.id.toString()
49    })
50    ```
51
52
53## Development Guidelines
54
55- Make no assumption on the order of item build functions. The execution order may not be the order of items inside the array.
56
57- Make no assumption either when items are built the first time. Currently initial render of **ForEach** builds all array items when the \@Component decorated component is rendered at the first time, but future framework versions might change this behaviour to a more lazy behaviour.
58
59- Using the **index** parameter has severe negative impact on the UI update performance. Avoid using this parameter whenever possible.
60
61- If the **index** parameter is used in the item generator function, it must also be used in the item index function. Otherwise, the framework counts in the index when generating the ID. By default, the index is concatenated to the end of the ID.
62
63
64## Application Scenarios
65
66
67### Simple ForEach Example
68
69This example creates three **\<Text>** and **\<Divide>** components based on the **arr** data.
70
71
72```ts
73@Entry
74@Component
75struct MyComponent {
76  @State arr: number[] = [10, 20, 30];
77
78  build() {
79    Column({ space: 5 }) {
80      Button('Reverse Array')
81        .onClick(() => {
82          this.arr.reverse();
83        })
84      ForEach(this.arr, (item: number) => {
85        Text(`item value: ${item}`).fontSize(18)
86        Divider().strokeWidth(2)
87      }, (item: number) => item.toString())
88    }
89  }
90}
91```
92
93
94### Complex ForEach Example
95
96
97```ts
98@Component
99struct CounterView {
100  @State label: string = "";
101  @State count: number = 0;
102
103  build() {
104    Button(`${this.label}-${this.count} click +1`)
105      .width(300).height(40)
106      .backgroundColor('#a0ffa0')
107      .onClick(() => {
108        this.count++;
109      })
110  }
111}
112
113@Entry
114@Component
115struct MainView {
116  @State arr: number[] = Array.from(Array(10).keys()); // [0.,.9]
117  nextUnused: number = this.arr.length;
118
119  build() {
120    Column() {
121      Button(`push new item`)
122        .onClick(() => {
123          this.arr.push(this.nextUnused++)
124        })
125        .width(300).height(40)
126      Button(`pop last item`)
127        .onClick(() => {
128          this.arr.pop()
129        })
130        .width(300).height(40)
131      Button(`prepend new item (unshift)`)
132        .onClick(() => {
133          this.arr.unshift(this.nextUnused++)
134        })
135        .width(300).height(40)
136      Button(`remove first item (shift)`)
137        .onClick(() => {
138          this.arr.shift()
139        })
140        .width(300).height(40)
141      Button(`insert at pos ${Math.floor(this.arr.length / 2)}`)
142        .onClick(() => {
143          this.arr.splice(Math.floor(this.arr.length / 2), 0, this.nextUnused++);
144        })
145        .width(300).height(40)
146      Button(`remove at pos ${Math.floor(this.arr.length / 2)}`)
147        .onClick(() => {
148          this.arr.splice(Math.floor(this.arr.length / 2), 1);
149        })
150        .width(300).height(40)
151      Button(`set at pos ${Math.floor(this.arr.length / 2)} to ${this.nextUnused}`)
152        .onClick(() => {
153          this.arr[Math.floor(this.arr.length / 2)] = this.nextUnused++;
154        })
155        .width(300).height(40)
156      ForEach(this.arr,
157        (item: string) => {
158          CounterView({ label: item.toString() })
159        },
160        (item: string) => item.toString()
161      )
162    }
163  }
164}
165```
166
167**MainView** has an \@State decorated array of numbers. Adding, deleting, and replacing array items are observed mutation events. Whenever one of these events occurs, **ForEach** in **MainView** is updated.
168
169The item index function creates a unique and persistent ID for each array item. The ArkUI framework uses this ID to determine whether an item in the array changes. As long as the ID is the same, the item value is assumed to remain unchanged, but its index position may have changed. For this mechanism to work, different array items cannot have the same ID.
170
171Using the item ID obtained through computation, the framework can distinguish newly created, removed, and retained array items.
172
1731. The framework removes UI components for a removed array item.
174
1752. The framework executes the item build function only for newly added array items.
176
1773. The framework does not execute the item build function for retained array items. If the item index within the array has changed, the framework will just move its UI components according to the new order, but will not update that UI components.
178
179The item index function is recommended, but optional. The generated IDs must be unique. This means that the same ID must not be computed for any two items within the array. The ID must be different even if the two array items have the same value.
180
181If the array item value changes, the ID must be changed.
182As mentioned earlier, the ID generation function is optional. The following example shows **ForEach** without the item index function:
183
184  ```ts
185let list: Object
186ForEach(this.arr,
187    (item: Object): string => {
188      list.label = item.toString();
189      CounterView(list)
190    }
191  )
192  ```
193
194If no item ID function is provided, the framework attempts to intelligently detect array changes when updating **ForEach**. However, it might remove child components and re-execute the item build function for array items that have been moved (with indexes changed). In the preceding example, this changes the application behavior in regard to the **counter** state of **CounterView**. When a new **CounterView** instance is created, the value of **counter** is initialized with **0**.
195
196
197### Example of ForEach Using \@ObjectLink
198
199If your application needs to preserve the state of repeated child components, you can use \@ObjectLink to enable the state to be "pushed up the component tree."
200
201
202```ts
203let NextID: number = 0;
204
205@Observed
206class MyCounter {
207  public id: number;
208  public c: number;
209
210  constructor(c: number) {
211    this.id = NextID++;
212    this.c = c;
213  }
214}
215
216@Component
217struct CounterView {
218  @ObjectLink counter: MyCounter;
219  label: string = 'CounterView';
220
221  build() {
222    Button(`CounterView [${this.label}] this.counter.c=${this.counter.c} +1`)
223      .width(200).height(50)
224      .onClick(() => {
225        this.counter.c += 1;
226      })
227  }
228}
229
230@Entry
231@Component
232struct MainView {
233  @State firstIndex: number = 0;
234  @State counters: Array<MyCounter> = [new MyCounter(0), new MyCounter(0), new MyCounter(0),
235    new MyCounter(0), new MyCounter(0)];
236
237  build() {
238    Column() {
239      ForEach(this.counters.slice(this.firstIndex, this.firstIndex + 3),
240        (item: MyCounter) => {
241          CounterView({ label: `Counter item #${item.id}`, counter: item })
242        },
243        (item: MyCounter) => item.id.toString()
244      )
245      Button(`Counters: shift up`)
246        .width(200).height(50)
247        .onClick(() => {
248          this.firstIndex = Math.min(this.firstIndex + 1, this.counters.length - 3);
249        })
250      Button(`counters: shift down`)
251        .width(200).height(50)
252        .onClick(() => {
253          this.firstIndex = Math.max(0, this.firstIndex - 1);
254        })
255    }
256  }
257}
258```
259
260When the value of **firstIndex** is increased, **ForEach** within **Mainview** is updated, and the **CounterView** child component associated with the item ID **firstIndex-1** is removed. For the array item whose ID is **firstindex + 3**, a new** CounterView** child component instance is created. The value of the state variable **counter** of the **CounterView** child component is preserved by the **Mainview** parent component. Therefore, **counter** is not rebuilt when the **CounterView** child component instance is rebuilt.
261
262> **NOTE**
263>
264> The most common mistake application developers make in connection with **ForEach** is that the ID generation function returns the same value for two array items, especially in the Array\<number> scenario.
265
266
267### Nested Use of ForEach
268
269While nesting **ForEach** inside another **ForEach** in the same component is allowed, it is not recommended. It is better to split the component into two and have each build function include just one ForEach. The following is a poor example of nested use of **ForEach**.
270
271
272```ts
273class Month {
274  year: number;
275  month: number;
276  days: number[];
277
278  constructor(year: number, month: number, ...days: number[]) {
279    this.year = year;
280    this.month = month;
281    this.days = days;
282  }
283}
284@Component
285struct CalendarExample {
286  // Simulate with six months.
287   arr28: Array<number> = Array(31).fill(0).map((_: number, i: number): number => i + 1);
288   arr30: Array<number> = Array(31).fill(0).map((_: number, i: number): number => i + 1);
289   arr31: Array<number> = Array(31).fill(0).map((_: number, i: number): number => i + 1);
290  @State calendar : Month[] = [
291    new Month(2020, 1, ...(this.arr31)),
292    new Month(2020, 2, ...(this.arr28)),
293    new Month(2020, 3, ...(this.arr31)),
294    new Month(2020, 4, ...(this.arr30)),
295    new Month(2020, 5, ...(this.arr31)),
296    new Month(2020, 6, ...(this.arr30))
297  ]
298  build() {
299    Column() {
300      Button() {
301        Text('next month')
302      }.onClick(() => {
303        this.calendar.shift()
304        this.calendar.push(new Month(2020, 7, ...(this.arr31)))
305      })
306      ForEach(this.calendar,
307        (item: Month) => {
308          ForEach(item.days,
309            (day : number) => {
310              // Build a date block.
311            },
312            (day : number) => day.toString()
313          )// Inner ForEach
314        },
315        (item: Month) => (item.year * 12 + item.month).toString() // This field is used together with the year and month as the unique ID of the month.
316      )// Outer ForEach
317    }
318  }
319}
320```
321
322The preceding example has two issues:
323
3241. The code readability is poor.
325
3262. For a data structure of months and days of a year, the framework cannot observe the attribute changes to **Month** objects, including any changes to the **days** array. As a result, the inner **ForEach** will not update the date.
327
328The recommended application design is to split **Calendar** into **Year**, **Month**, and **Day** child components. Define a **Day** model class to hold information about a day and decorate the class with \@Observed. The **DayView** component uses an \@ObjectLink decorated variable to link to the data about a day. Perform the same operations on the **MonthView** and **Month** model classes.
329
330
331### Example of Using the Optional index Parameter in ForEach
332
333You can use the optional **index** parameter in item build and ID generation functions.
334
335
336```ts
337@Entry
338@Component
339struct ForEachWithIndex {
340  @State arr: number[] = [4, 3, 1, 5];
341
342  build() {
343    Column() {
344      ForEach(this.arr,
345        (it: number, index) => {
346          Text(`Item: ${index} - ${it}`)
347        },
348        (it: number, index) => {
349          return `${index} - ${it}`
350        }
351      )
352    }
353  }
354}
355```
356
357The correct construction of the ID generation function is essential. When **index** is used in the item generation function, it should also be used in the ID generation function to produce unique IDs and an ID for given source array item that changes when its index position within the array changes.
358
359This example also illustrates that the **index** parameter can cause significant performance degradation. If an item is moved in the source array without modification, the dependent UI still requires rebuilding because of the changed index. For example, with the use of index sorting, the array only requires the unmodified child UI node of **ForEach** to be moved to the correct slot, which is a lightweight operation for the framework. When **index** is used, all child UI nodes need to be rebuilt, which is much more heavy weight.
360