• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# ForEach: Rendering Repeated Content
2
3**ForEach** enables array-based rendering of repeated content. It must be used in a container component, and the component it returns must be one allowed inside the container component. For example, for rendering of list items, **ForEach** must be used in the [List](../../reference/apis-arkui/arkui-ts/ts-container-list.md) component.
4
5For details about API parameters, see [ForEach](../../reference/apis-arkui/arkui-ts/ts-rendering-control-foreach.md).
6
7> **NOTE**
8>
9> This API can be used in ArkTS widgets since API version 9.
10
11## Key Generation Rules
12
13During **ForEach** rendering, the system generates a unique and persistent key for each array element to identify the corresponding component. When the key changes, the ArkUI framework considers that the array element has been replaced or modified and creates a component based on the new key.
14
15**ForEach** provides a parameter named **keyGenerator**, which is a function that allows you to customize key generation rules. If no **keyGenerator** function is defined, the ArkUI framework uses the default key generation format **(item: Object, index: number) => { return index + '__' + JSON.stringify(item); }**.
16
17The ArkUI framework follows specific rules for key generation in **ForEach**, which are primarily related to the **itemGenerator** function and the second parameter **index** of the **keyGenerator** function. The specific key generation rule judgment logic is shown in the following figure.
18
19**Figure 1** ForEach key generation rules
20![ForEach-Key-Generation-Rules](figures/ForEach-Key-Generation-Rules.png)
21
22> **NOTE**
23>
24> The ArkUI framework issues warnings for duplicate keys. If duplicate keys exist during UI re-rendering, the framework may not work properly. For details, see [Unexpected Rendering Results](#unexpected-rendering-results).
25
26## Component Creation Rules
27
28After the key generation rules are determined, the **itemGenerator** function – the second parameter in **ForEach** – creates a component for each array item of the data source based on the rules. Component creation involves two scenarios: [initial rendering](#initial-rendering) and [non-initial rendering](#non-initial-rendering).
29
30### Initial Rendering
31
32When used for initial rendering, **ForEach** generates a unique key for each array item of the data source based on the key generation rules, and creates a component.
33
34```ts
35@Entry
36@Component
37struct Parent {
38  @State simpleList: Array<string> = ['one', 'two', 'three'];
39
40  build() {
41    Row() {
42      Column() {
43        ForEach(this.simpleList, (item: string) => {
44          ChildItem({ item: item })
45        }, (item: string) => item)
46      }
47      .width('100%')
48      .height('100%')
49    }
50    .height('100%')
51    .backgroundColor(0xF1F3F5)
52  }
53}
54
55@Component
56struct ChildItem {
57  @Prop item: string;
58
59  build() {
60    Text(this.item)
61      .fontSize(50)
62  }
63}
64```
65
66The figure below shows the effect.
67
68**Figure 2** Initial rendering result for the ForEach data source without duplicate keys
69![ForEach-CaseStudy-1stRender-NoDup](figures/ForEach-CaseStudy-1stRender-NoDup.png)
70
71In the preceding code, the return value of the **keyGenerator** function is **item**. During **ForEach** rendering, keys (**one**, **two**, and **three**) are generated in sequence for the array items, and the corresponding **ChildItem** components are created and rendered to the UI.
72
73When the keys generated for different array items are the same, the behavior of the framework is undefined. For example, in the following code, when data items with the same key **two** are rendered by **ForEach**, only one **ChildItem** component, instead of multiple components with the same key, is created.
74
75```ts
76@Entry
77@Component
78struct Parent {
79  @State simpleList: Array<string> = ['one', 'two', 'two', 'three'];
80
81  build() {
82    Row() {
83      Column() {
84        ForEach(this.simpleList, (item: string) => {
85          ChildItem({ item: item })
86        }, (item: string) => item)
87      }
88      .width('100%')
89      .height('100%')
90    }
91    .height('100%')
92    .backgroundColor(0xF1F3F5)
93  }
94}
95
96@Component
97struct ChildItem {
98  @Prop item: string;
99
100  build() {
101    Text(this.item)
102      .fontSize(50)
103  }
104}
105 ```
106
107The figure below shows the effect.
108
109**Figure 3** Initial rendering result for the ForEach data source with duplicate keys
110![ForEach-CaseStudy-1stRender-Dup](figures/ForEach-CaseStudy-1stRender-Dup.png)
111
112In this example, the final key value generation rule is **item**. When **ForEach** iterates over the data source **simpleList**, it creates a component with the key **two** and records it when it encounters the **two** at index 1. When it encounters the **two** at index 2, as the key for the current item is also **two**, no new component is created.
113
114### Non-Initial Rendering
115
116When **ForEach** is used for re-rendering (non-initial rendering), it checks whether the newly generated key already exists in the previous rendering. If the key does not exist, a new component is created. If the key exists, no new component is created, and the component corresponding to the key is re-rendered. For example, in the following code snippet, changing the value of the array's third item to **"new three"** through clicking triggers non-initial rendering by **ForEach**.
117
118```ts
119@Entry
120@Component
121struct Parent {
122  @State simpleList: Array<string> = ['one', 'two', 'three'];
123
124  build() {
125    Row() {
126      Column() {
127        Text('Click to change the value of the third array item')
128          .fontSize(24)
129          .fontColor(Color.Red)
130          .onClick(() => {
131            this.simpleList[2] = 'new three';
132          })
133
134        ForEach(this.simpleList, (item: string) => {
135          ChildItem({ item: item })
136            .margin({ top: 20 })
137        }, (item: string) => item)
138      }
139      .justifyContent(FlexAlign.Center)
140      .width('100%')
141      .height('100%')
142    }
143    .height('100%')
144    .backgroundColor(0xF1F3F5)
145  }
146}
147
148@Component
149struct ChildItem {
150  @Prop item: string;
151
152  build() {
153    Text(this.item)
154      .fontSize(30)
155  }
156}
157```
158
159The figure below shows the effect.
160
161**Figure 4** Re-rendering with ForEach
162![ForEach-Non-Initial-Render-Case-Effect](figures/ForEach-Non-Initial-Render-Case-Effect.gif)
163
164This example demonstrates that @State can observe changes to the items of a primitive data type array, such as **simpleList**.
165
1661. When any item in **simpleList** changes, **ForEach** is triggered for re-rendering.
1672. **ForEach** iterates through the new data source **['one', 'two', 'new three']** and generates the corresponding keys **one**, **two**, and **new three**.
1683. For keys **one** and **two** that exist in the previous rendering, **ForEach** reuses the corresponding components and re-renders them. For the third array item **"new three"**, since its generated key **new three** does not exist in the previous rendering, **ForEach** creates a component for it.
169
170## Use Cases
171
172**ForEach** is typically used in several cases: [static data source](#static-data-source), [changes of data source array items](#changes-of-data-source-array-items) (for example, insertions or deletions), and [property changes in data source array items](#property-changes-in-data-source-array-items).
173
174### Static Data Source
175
176If the data source remains unchanged, it can of a primitive data type. For example, when implementing loading states, you can render skeleton screens using a static list.
177
178```ts
179@Entry
180@Component
181struct ArticleList {
182  @State simpleList: Array<number> = [1, 2, 3, 4, 5];
183
184  build() {
185    Column() {
186      ForEach(this.simpleList, (item: number) => {
187        ArticleSkeletonView()
188          .margin({ top: 20 })
189      }, (item: number) => item.toString())
190    }
191    .padding(20)
192    .width('100%')
193    .height('100%')
194  }
195}
196
197@Builder
198function textArea(width: number | Resource | string = '100%', height: number | Resource | string = '100%') {
199  Row()
200    .width(width)
201    .height(height)
202    .backgroundColor('#FFF2F3F4')
203}
204
205@Component
206struct ArticleSkeletonView {
207  build() {
208    Row() {
209      Column() {
210        textArea(80, 80)
211      }
212      .margin({ right: 20 })
213
214      Column() {
215        textArea('60%', 20)
216        textArea('50%', 20)
217      }
218      .alignItems(HorizontalAlign.Start)
219      .justifyContent(FlexAlign.SpaceAround)
220      .height('100%')
221    }
222    .padding(20)
223    .borderRadius(12)
224    .backgroundColor('#FFECECEC')
225    .height(120)
226    .width('100%')
227    .justifyContent(FlexAlign.SpaceBetween)
228  }
229}
230```
231
232The figure below shows the effect.
233
234**Figure 5** Skeleton screen
235![ForEach-SkeletonScreen](figures/ForEach-SkeletonScreen.png)
236
237In this example, the data item is used as the key generation rule. Because the array items of the data source **simpleList** are different, the uniqueness of the keys can be ensured.
238
239### Changes of Data Source Array Items
240
241If data source array item changes, for example, when an array item is inserted or deleted, or has its index changed, the data source should be of the object array type, and a unique ID of the object is used as the key.
242
243```ts
244class Article {
245  id: string;
246  title: string;
247  brief: string;
248
249  constructor(id: string, title: string, brief: string) {
250    this.id = id;
251    this.title = title;
252    this.brief = brief;
253  }
254}
255
256@Entry
257@Component
258struct ArticleListView {
259  @State isListReachEnd: boolean = false;
260  @State articleList: Array<Article> = [
261    new Article('001', 'Article 1', 'Abstract'),
262    new Article('002', 'Article 2', 'Abstract'),
263    new Article('003', 'Article 3', 'Abstract'),
264    new Article('004', 'Article 4', 'Abstract'),
265    new Article('005', 'Article 5', 'Abstract'),
266    new Article('006', 'Article 6', 'Abstract')
267  ];
268
269  loadMoreArticles() {
270    this.articleList.push(new Article('007', 'New article', 'Abstract'));
271  }
272
273  build() {
274    Column({ space: 5 }) {
275      List() {
276        ForEach(this.articleList, (item: Article) => {
277          ListItem() {
278            ArticleCard({ article: item })
279              .margin({ top: 20 })
280          }
281        }, (item: Article) => item.id)
282      }
283      .onReachEnd(() => {
284        this.isListReachEnd = true;
285      })
286      .parallelGesture(
287        PanGesture({ direction: PanDirection.Up, distance: 80 })
288          .onActionStart(() => {
289            if (this.isListReachEnd) {
290              this.loadMoreArticles();
291              this.isListReachEnd = false;
292            }
293          })
294      )
295      .padding(20)
296      .scrollBar(BarState.Off)
297    }
298    .width('100%')
299    .height('100%')
300    .backgroundColor(0xF1F3F5)
301  }
302}
303
304@Component
305struct ArticleCard {
306  @Prop article: Article;
307
308  build() {
309    Row() {
310      // 'app.media.icon' is only an example. Replace it with the actual one in use. Otherwise, the imageSource instance fails to be created, and subsequent operations cannot be performed.
311      Image($r('app.media.icon'))
312        .width(80)
313        .height(80)
314        .margin({ right: 20 })
315
316      Column() {
317        Text(this.article.title)
318          .fontSize(20)
319          .margin({ bottom: 8 })
320        Text(this.article.brief)
321          .fontSize(16)
322          .fontColor(Color.Gray)
323          .margin({ bottom: 8 })
324      }
325      .alignItems(HorizontalAlign.Start)
326      .width('80%')
327      .height('100%')
328    }
329    .padding(20)
330    .borderRadius(12)
331    .backgroundColor('#FFECECEC')
332    .height(120)
333    .width('100%')
334    .justifyContent(FlexAlign.SpaceBetween)
335  }
336}
337```
338
339The following figure shows the initial screen (on the left) and the screen after a pull-to-refresh gesture (on the right).
340
341**Figure 6** Effect when the data source is changed
342![ForEach-DataSourceArrayChange](figures/ForEach-DataSourceArrayChange.png)
343
344In this example, the **ArticleCard** component serves as a child component of the **ArticleListView** component and receives an **Article** object through the @Prop decorator to render article cards.
345
3461. When the list scrolls to the bottom with a swipe distance exceeding 80 vp, the **loadMoreArticles()** function is invoked. This function appends new elements to the **articleList** data source, increasing its length.
3472. Because the data source is decorated by @State, the ArkUI framework can detect changes in the data source length and trigger **ForEach** for re-rendering.
348
349### Property Changes in Data Source Array Items
350
351When the items in the data source decorated with @State are of a complex data type (for example, objects), the ArkUI framework cannot detect changes to the properties of the items in the data source. As a result, modifying certain properties of the items will not trigger re-rendering of **ForEach**. To achieve re-rendering of **ForEach** in such cases, use the @Observed and @ObjectLink decorators. The following example illustrates a typical use case, where clicking the like icon on an article card updates the like count of the article.
352
353```ts
354@Observed
355class Article {
356  id: string;
357  title: string;
358  brief: string;
359  isLiked: boolean;
360  likesCount: number;
361
362  constructor(id: string, title: string, brief: string, isLiked: boolean, likesCount: number) {
363    this.id = id;
364    this.title = title;
365    this.brief = brief;
366    this.isLiked = isLiked;
367    this.likesCount = likesCount;
368  }
369}
370
371@Entry
372@Component
373struct ArticleListView {
374  @State articleList: Array<Article> = [
375    new Article('001', 'Article 0', 'Abstract', false, 100),
376    new Article('002', 'Article 1', 'Abstract', false, 100),
377    new Article('003', 'Article 2', 'Abstract', false, 100),
378    new Article('004', 'Article 4', 'Abstract', false, 100),
379    new Article('005', 'Article 5', 'Abstract', false, 100),
380    new Article('006', 'Article 6', 'Abstract', false, 100),
381  ];
382
383  build() {
384    List() {
385      ForEach(this.articleList, (item: Article) => {
386        ListItem() {
387          ArticleCard({
388            article: item
389          })
390            .margin({ top: 20 })
391        }
392      }, (item: Article) => item.id)
393    }
394    .padding(20)
395    .scrollBar(BarState.Off)
396    .backgroundColor(0xF1F3F5)
397  }
398}
399
400@Component
401struct ArticleCard {
402  @ObjectLink article: Article;
403
404  handleLiked() {
405    this.article.isLiked = !this.article.isLiked;
406    this.article.likesCount = this.article.isLiked ? this.article.likesCount + 1 : this.article.likesCount - 1;
407  }
408
409  build() {
410    Row() {
411      // 'app.media.icon' is only an example. Replace it with the actual one in use. Otherwise, the imageSource instance fails to be created, and subsequent operations cannot be performed.
412      Image($r('app.media.icon'))
413        .width(80)
414        .height(80)
415        .margin({ right: 20 })
416
417      Column() {
418        Text(this.article.title)
419          .fontSize(20)
420          .margin({ bottom: 8 })
421        Text(this.article.brief)
422          .fontSize(16)
423          .fontColor(Color.Gray)
424          .margin({ bottom: 8 })
425
426        Row() {
427          // 'app.media.iconLiked' and 'app.media.iconUnLiked' are only an example. Replace it with the actual one in use. Otherwise, the imageSource instance fails to be created, and subsequent operations cannot be performed.
428          Image(this.article.isLiked ? $r('app.media.iconLiked') : $r('app.media.iconUnLiked'))
429            .width(24)
430            .height(24)
431            .margin({ right: 8 })
432          Text(this.article.likesCount.toString())
433            .fontSize(16)
434        }
435        .onClick(() => this.handleLiked())
436        .justifyContent(FlexAlign.Center)
437      }
438      .alignItems(HorizontalAlign.Start)
439      .width('80%')
440      .height('100%')
441    }
442    .padding(20)
443    .borderRadius(12)
444    .backgroundColor('#FFECECEC')
445    .height(120)
446    .width('100%')
447    .justifyContent(FlexAlign.SpaceBetween)
448  }
449}
450```
451
452The following figure shows the initial screen (on the left) and the screen after the like icon of Article 1 is clicked (on the right).
453
454**Figure 7** Effect when properties of data source array items are changed
455![ForEach-DataSourceArraySubpropertyChange](figures/ForEach-DataSourceArraySubpropertyChange.png)
456
457In this example, the **Article** class is decorated by the @Observed decorator. The parent component **ArticleListView** passes an **Article** instance to the child component **ArticleCard**, which receives the instance using the @ObjectLink decorator.
458
4591. When the like icon of Article 1 is clicked, the **handleLiked** function of the **ArticleCard** component is triggered. This function changes the values of the **isLiked** and **likesCount** properties of the **article** instance in the component pertaining to Article 1.
4602. The article instance is an @ObjectLink decorated state variable. Changes to its property values trigger the re-rendering of the **ArticleCard** component, which then reads the new values of **isLiked** and **likesCount**.
461
462### Drag-and-Drop Sorting
463By using **ForEach** within a List component and setting up the **onMove** event, you can implement drag-and-drop sorting. When the drag-and-drop gesture is released, if any item's position changes, the **onMove** event is triggered, which reports the original index and target index of the relocated item. In the **onMove** event, the data source must be updated based on the reported start index and target index. Before and after the data source is modified, the key value of each item must remain unchanged to ensure that the drop animation can be executed properly.
464
465```ts
466@Entry
467@Component
468struct ForEachSort {
469  @State arr: Array<string> = [];
470
471  build() {
472    Column() {
473      // Clicking this button triggers re-rendering of ForEach.
474      Button('Add one item')
475        .onClick(() => {
476          this.arr.push('10');
477        })
478        .width(300)
479        .margin(10)
480
481      List() {
482        ForEach(this.arr, (item: string) => {
483          ListItem() {
484            Text(item.toString())
485              .fontSize(16)
486              .textAlign(TextAlign.Center)
487              .size({ height: 100, width: "100%" })
488          }.margin(10)
489          .borderRadius(10)
490          .backgroundColor("#FFFFFFFF")
491        }, (item: string) => item)
492          .onMove((from: number, to: number) => {
493            // The following two lines ensure that the order of components on the screen matches the order of items in the array arr.
494            // If these lines are commented out, after the first drag-and-drop sorting and subsequent addition of an item to the end of arr, the on-screen component order will align with the arr item sequence—rather than the order established by the initial drag-and-drop. This results in the loss of drag-and-drop sorting after re-rendering.
495            let tmp = this.arr.splice(from, 1);
496            this.arr.splice(to, 0, tmp[0]);
497          })
498      }
499      .width('100%')
500      .height('100%')
501      .backgroundColor("#FFDCDCDC")
502    }
503  }
504
505  aboutToAppear(): void {
506    for (let i = 0; i < 10; i++) {
507      this.arr.push(i.toString());
508    }
509  }
510}
511```
512
513**Figure 8** Effect of ForEach drag-and-drop sorting
514![ForEach-Drag-Sort](figures/ForEach-Drag-Sort.gif)
515
516If the two lines in the **onMove** event handler are commented out, the effect of clicking **Add one item** and triggering re-rendering is shown in the following figure.
517
518**Figure 9** ForEach drag-and-drop sorting effect not preserved after re-rendering
519![ForEach-Drag-Sort](figures/ForEach-Drag-Sort2.PNG)
520
521## Recommendations
522
523- To ensure key uniqueness for object data, use a unique **id** property from the object data as the key.
524- Do not use the data item **index** as the key, as this can cause [unexpected rendering results](#unexpected-rendering-results) and [reduced rendering performance](#reduced-rendering-performance). If **index** must be used (for example, for conditional rendering based on **index**), be aware that data source changes will force **ForEach** to re-create components, incurring a performance cost.
525- For arrays of primitive data types, which do not have a unique ID property: If using the data item itself as the key, ensure that no duplicate values exist. For mutable data sources, convert the array to objects with unique ID properties, then use the ID as the key.
526- The **index** parameter serves as a fallback to ensure key uniqueness. When modifying a data item, since the **item** parameter in **itemGenerator** is immutable, use **index** to update the data source and trigger UI re-rendering.
527- Within [List](../../reference/apis-arkui/arkui-ts/ts-container-list.md), [Grid](../../reference/apis-arkui/arkui-ts/ts-container-grid.md), [Swiper](../../reference/apis-arkui/arkui-ts/ts-container-swiper.md), and [WaterFlow](../../reference/apis-arkui/arkui-ts/ts-container-waterflow.md) containers, do not use **ForEach** together with [LazyForEach](./arkts-rendering-control-lazyforeach.md).
528- When dealing with a large number of child components, **ForEach** can lead to performance issues such as lag or jank. In such cases, consider using [LazyForEach](./arkts-rendering-control-lazyforeach.md) instead. For details about the best practice, see [Performance Optimization Using LazyForEach](https://developer.huawei.com/consumer/en/doc/best-practices/bpta-lazyforeach-optimization).
529- When array items are objects, do not replace old items with new objects of the same content. If an item changes but the key remains the same, the framework may not detect the change, leading to [unrendered data updates](#data-changes-failing-to-trigger-rendering).
530## Common Pitfalls
531
532Incorrect usage of **ForEach** keys can lead to functional and performance issues, causing unexpected rendering behavior. For detailed examples, see [Unexpected Rendering Results](#unexpected-rendering-results) and [Reduced Rendering Performance](#reduced-rendering-performance).
533
534### Unexpected Rendering Results
535
536In this example, the **KeyGenerator** function, which is the third parameter of **ForEach**, is set to use the string-type **index** property of the data source as the key generation rule. When **Insert Item After First Item** in the parent component **Parent** is clicked, an unexpected result is displayed.
537
538```ts
539@Entry
540@Component
541struct Parent {
542  @State simpleList: Array<string> = ['one', 'two', 'three'];
543
544  build() {
545    Column() {
546      Button() {
547        Text('Insert Item After First Item').fontSize(30)
548      }
549      .onClick(() => {
550        this.simpleList.splice(1, 0, 'new item');
551      })
552
553      ForEach(this.simpleList, (item: string) => {
554        ChildItem({ item: item })
555      }, (item: string, index: number) => index.toString())
556    }
557    .justifyContent(FlexAlign.Center)
558    .width('100%')
559    .height('100%')
560    .backgroundColor(0xF1F3F5)
561  }
562}
563
564@Component
565struct ChildItem {
566  @Prop item: string;
567
568  build() {
569    Text(this.item)
570      .fontSize(30)
571  }
572}
573```
574
575The following figure shows the initial screen and the screen after **Insert Item After First Item** is clicked.
576
577**Figure 10** Unexpected rendering result
578![ForEach-UnexpectedRenderingResult](figures/ForEach-UnexpectedRenderingResult.gif)
579
580When **ForEach** is used for initial rendering, the created keys are **0**, **1**, and **2** in sequence.
581
582After a new item is inserted, the data source **simpleList** changes to ['one','new item', 'two', 'three']. The ArkUI framework detects changes in the length of the @State decorated data source and triggers **ForEach** for re-rendering.
583
584**ForEach** traverses items in the new data source. When it reaches array item **one**, it generates key **0** for the item, and because the same key already exists, no new component is created. When **ForEach** reaches array item **new item**, it generates key **1** for the item, and because the same key already exists, no new component is created. When **ForEach** reaches array item **two**, it generates key **2** for the item, and because the same key already exists, no new component is created. When **ForEach** reaches array item **three**, it generates key **3** for the item, and because no same key exists, a new component **three** is created.
585
586In the preceding example, the final key generation rule includes **index**. While the expected rendering result is **['one','new item', 'two', 'three']**, the actual rendering result is **['one', 'two', 'three', 'three']**. Therefore, to avoid unexpected rendering results, avoid using index-based keys for **ForEach**.
587
588### Reduced Rendering Performance
589
590In this example, the **KeyGenerator** function, which is the third parameter of **ForEach**, is omitted. According to the description in [Key Generation Rules](#key-generation-rules), the framework uses the default key format **index + '__' + JSON.stringify(item)**. Clicking the **Insert Item After First Item** button triggers component re-creation for the second array item and all subsequent items.
591
592```ts
593@Entry
594@Component
595struct Parent {
596  @State simpleList: Array<string> = ['one', 'two', 'three'];
597
598  build() {
599    Column() {
600      Button() {
601        Text('Insert Item After First Item').fontSize(30)
602      }
603      .onClick(() => {
604        this.simpleList.splice(1, 0, 'new item');
605        console.info(`[onClick]: simpleList is [${this.simpleList.join(', ')}]`);
606      })
607
608      ForEach(this.simpleList, (item: string) => {
609        ChildItem({ item: item })
610      })
611    }
612    .justifyContent(FlexAlign.Center)
613    .width('100%')
614    .height('100%')
615    .backgroundColor(0xF1F3F5)
616  }
617}
618
619@Component
620struct ChildItem {
621  @Prop item: string;
622
623  aboutToAppear() {
624    console.info(`[aboutToAppear]: item is ${this.item}`);
625  }
626
627  build() {
628    Text(this.item)
629      .fontSize(50)
630  }
631}
632```
633
634The following figure shows the initial screen and the screen after **Insert Item After First Item** is clicked.
635
636**Figure 11** Reduced rendering performance
637![ForEach-RenderPerformanceDecrease](figures/ForEach-RenderPerformanceDecrease.gif)
638
639After **Insert Item After First Item** is clicked, DevEco Studio displays logs as shown in the figure below.
640
641**Figure 12** Logs indicating reduced rendering performance
642![ForEach-RenderPerformanceDecreaseLogs](figures/ForEach-RenderPerformanceDecreaseLogs.png)
643
644After a new item is inserted, **ForEach** creates the corresponding **ChildItem** components for the **new item**, **two**, and **three** array items, and executes the [aboutToAppear()](../../reference/apis-arkui/arkui-ts/ts-custom-component-lifecycle.md#abouttoappear) callback. Here are the reasons:
645
6461. During initial rendering, **ForEach** generates keys **0__one**, **1__two**, and **2__three** for the initial data source ['one', 'two', 'three'].
6472. After a new item is inserted, the data source **simpleList** changes to ['one','new item', 'two', 'three']. The ArkUI framework detects changes in the length of the @State decorated data source and triggers **ForEach** for re-rendering.
6483. **ForEach** traverses items in the new data source. When it reaches array item **one**, it generates key **0__one** for the item, and because the same key already exists, no new component is created. When **ForEach** reaches array item **new item**, it generates key **1__new item** for the item, and because no same key exists, a new component **new item** is created. When **ForEach** reaches array item **two**, it generates key **2__two** for the item, and because no same key exists, a new component **two** is created. When **ForEach** reaches array item **three**, it generates key **3__three** for the item, and because no same key exists, a new component **three** is created.
649
650Although the UI rendering results in this example meet expectations, **ForEach** will re-create components for the inserted item and all subsequent items every time a new item is inserted into the middle of the array. When the data source is large or components are complex, this inability to reuse components leads to performance degradation. Therefore, avoid omitting the third parameter (**KeyGenerator**) and do not use the data index (**index**) as the key.
651
652The correct way to use **ForEach** for optimal rendering and efficiency is as follows:
653```ts
654ForEach(this.simpleList, (item: string) => {
655  ChildItem({ item: item })
656}, (item: string) => item)  // Ensure that the key is unique.
657```
658With the use of **KeyGenerator** function, different keys are generated for different data items of the data source, and the same key is generated for the same data item each time.
659
660### Data Changes Failing to Trigger Rendering
661When the **Like/Unlike first article** button is clicked, the first component toggles the like gesture and updates the like count. However, if the **Replace first article** button is clicked first, the **Like/Unlike first article** button does not take effect. The reason is that replacing **articleList[0]** changes the state variable **articleList**, triggering re-rendering of **ForEach**. However, since the key for the new articleList[0] remains unchanged, **ForEach** does not update the data to the child component. As a result, the first component remains bound to the old **articleList[0]**. When the property of the new **articleList[0]** is changed, the first component cannot detect the change and does not trigger re-rendering. Clicking the like icon can trigger rendering. This is because the property of the array item bound to the component is changed, the component detects the change and renders it again.
662
663```ts
664@Observed
665class Article {
666  id: string;
667  title: string;
668  brief: string;
669  isLiked: boolean;
670  likesCount: number;
671
672  constructor(id: string, title: string, brief: string, isLiked: boolean, likesCount: number) {
673    this.id = id;
674    this.title = title;
675    this.brief = brief;
676    this.isLiked = isLiked;
677    this.likesCount = likesCount;
678  }
679}
680
681@Entry
682@Component
683struct ArticleListView {
684  @State articleList: Array<Article> = [
685    new Article('001', 'Article 0', 'Abstract', false, 100),
686    new Article('002', 'Article 1', 'Abstract', false, 100),
687    new Article('003', 'Article 2', 'Abstract', false, 100),
688    new Article('004', 'Article 4', 'Abstract', false, 100),
689    new Article('005', 'Article 5', 'Abstract', false, 100),
690    new Article('006', 'Article 6', 'Abstract', false, 100),
691  ];
692
693  build() {
694    Column() {
695      Button('Replace first article')
696        .onClick(() => {
697          this.articleList[0] = new Article('001', 'Article 0', 'Abstract', false, 100);
698        })
699        .width(300)
700        .margin(10)
701
702      Button('Like/Unlike first article')
703        .onClick(() => {
704          this.articleList[0].isLiked = !this.articleList[0].isLiked;
705          this.articleList[0].likesCount =
706            this.articleList[0].isLiked ? this.articleList[0].likesCount + 1 : this.articleList[0].likesCount - 1;
707        })
708        .width(300)
709        .margin(10)
710
711      List() {
712        ForEach(this.articleList, (item: Article) => {
713          ListItem() {
714            ArticleCard({
715              article: item
716            })
717              .margin({ top: 20 })
718          }
719        }, (item: Article) => item.id)
720      }
721      .padding(20)
722      .scrollBar(BarState.Off)
723      .backgroundColor(0xF1F3F5)
724    }
725  }
726}
727
728@Component
729struct ArticleCard {
730  @ObjectLink article: Article;
731
732  handleLiked() {
733    this.article.isLiked = !this.article.isLiked;
734    this.article.likesCount = this.article.isLiked ? this.article.likesCount + 1 : this.article.likesCount - 1;
735  }
736
737  build() {
738    Row() {
739      // 'app.media.icon' is only an example. Replace it with the actual one in use. Otherwise, the imageSource instance fails to be created, and subsequent operations cannot be performed.
740      Image($r('app.media.icon'))
741        .width(80)
742        .height(80)
743        .margin({ right: 20 })
744
745      Column() {
746        Text(this.article.title)
747          .fontSize(20)
748          .margin({ bottom: 8 })
749        Text(this.article.brief)
750          .fontSize(16)
751          .fontColor(Color.Gray)
752          .margin({ bottom: 8 })
753
754        Row() {
755          // 'app.media.iconLiked' and 'app.media.iconUnLiked' are only examples. Replace them with the actual ones in use. Otherwise, the imageSource instance fails to be created, and subsequent operations cannot be performed.
756          Image(this.article.isLiked ? $r('app.media.iconLiked') : $r('app.media.iconUnLiked'))
757            .width(24)
758            .height(24)
759            .margin({ right: 8 })
760          Text(this.article.likesCount.toString())
761            .fontSize(16)
762        }
763        .onClick(() => this.handleLiked())
764        .justifyContent(FlexAlign.Center)
765      }
766      .alignItems(HorizontalAlign.Start)
767      .width('80%')
768      .height('100%')
769    }
770    .padding(20)
771    .borderRadius(12)
772    .backgroundColor('#FFECECEC')
773    .height(120)
774    .width('100%')
775    .justifyContent(FlexAlign.SpaceBetween)
776  }
777}
778```
779**Figure 13** Data changes failing to trigger rendering
780![ForEach-StateVarNoRender](figures/ForEach-StateVarNoRender.PNG)
781
782### Unnecessary Memory Consumption
783If no **keyGenerator** function is defined, the ArkUI framework uses the default key generation format **(item: Object, index: number) => { return index + '__' + JSON.stringify(item); }**. When **item** is a complex object, serializing it to a JSON string results in a long string that consumes more memory.
784
785```ts
786class Data {
787  longStr: string;
788  key: string;
789
790  constructor(longStr: string, key: string) {
791    this.longStr = longStr;
792    this.key = key;
793  }
794}
795
796@Entry
797@Component
798struct Parent {
799  @State simpleList: Array<Data> = [];
800
801  aboutToAppear(): void {
802    let longStr = '';
803    for (let i = 0; i < 2000; i++) {
804      longStr += i.toString();
805    }
806    for (let index = 0; index < 3000; index++) {
807      let data: Data = new Data(longStr, 'a' + index.toString());
808      this.simpleList.push(data);
809    }
810  }
811
812  build() {
813    List() {
814      ForEach(this.simpleList, (item: Data) => {
815        ListItem() {
816          Text(item.key)
817        }
818      }
819        // If the keyGenerator function is not defined, the ArkUI framework uses the default key value generation function.
820        , (item: Data) => {
821          return item.key;
822        }
823      )
824    }.height('100%')
825    .width('100%')
826  }
827}
828```
829
830A comparison of memory usage between the default and custom **keyGenerator** implementations shows a reduction of approximately 70 MB when a custom function is used.
831
832**Figure 14** Memory usage with default key generation
833![ForEach-StateVarNoRender](figures/ForEach-default-keyGenerator.PNG)
834
835**Figure 15** Memory usage with custom key generation
836![ForEach-StateVarNoRender](figures/ForEach-defined-keyGenerator.PNG)
837