Sự quan trọng của key props trong ReactJS

Đặt vấn đề

Nếu bạn đã tìm hiểu hay đang làm việc với ReactJS, mình đoán có lẽ bạn đã từng bắt gặp cảnh báo:

khi đang map() một mảng danh sách nào đó.

Uhmm thì... sửa theo Stackoverflow là được rồi 😹😹 Có bao giờ bạn tự hỏi vì sao phải làm như vậy không?

Trong bài viết này chúng ta sẽ cùng trả lời cho câu hỏi đó nhé !

Lý do ReactJS cần key props

Khi state trong component thay đổi, hàm render() sẽ trả về một tree mới (a new tree of React elements), khác với tree cũ lúc state chưa thay đổi. ReactJS sẽ tìm ra những điểm khác biệt thông qua thuật toán diffingupdate lại chúng trên UI. Quá trình match tree cũ và tree mới gọi là reconciliation. Bạn có thể tìm hiểu thêm chi tiết về cơ chế ReactJS hoạt động với Virtual DOM trong bài viết này của mình.

Quay lại với chủ đề của chúng ta: Chính xác thì có ảnh hưởng gì của mảng mà ReactJS cần có key props?

Giả sử, chúng ta render một mảng mà không thêm key props:

<li>Devnote 1</li>
<li>Devnote 2</li>

Sau đó, Devnote 3 được thêm vào. Lúc này:

<li>Devnote 1</li>
<li>Devnote 2</li>
<li>Devnote 3</li>

ReactJS bắt đầu so sánh 2 trees này để tìm ra điểm khác biệt. Để thực hiện điều này, nó sẽ cùng-lặp-qua-lần lượt-tất-cả các phần tử con của cả 2 mảng; generate ra chỗ cần cập nhật mỗi khi nhận ra được điểm nào đó khác nhau.

Như vậy thì ở ví dụ trên, phần tử đầu tiên và phần tử thứ 2, okie, giống nhau, phần tử thứ 3 là chỗ được thay đổi, cập nhật thôi ! Nom có vẻ ngon lành cành đào nhỉ 😸😸

Bây giờ, chúng ta lại thêm Devnote 0 vào trước mảng đó:

<li>Devnote 0</li>
<li>Devnote 1</li>
<li>Devnote 2</li>
<li>Devnote 3</li>

Một lần nữa, ReactJS lại lặp, lại so sánh:

  • Phần tử đầu của old tree(<li>Devnote 1</li>) với phần tử đầu của new tree(<li>Devnote 0</li>) ⇒ khác nhau ⇒ cập nhật.
  • Phần tử thứ 2 của old tree(<li>Devnote 2</li>) với phần tử thứ 2 của new tree(<li>Devnote 1</li>) ⇒ khác nhau ⇒ cập nhật.
  • ...

Ồ, có vẻ như có gì đó phát sinh rồi đây !

Cứ vậy thì ReactJS sẽ cập nhật lại hết tất cả các phần tử con thay vì nhận ra được <li>Devnote 1</li>, <li>Devnote 2</li>, <li>Devnote 3</li> không thay đổi. Điều này sẽ ảnh hưởng tới performance của ứng dụng.

Lúc này, key props sinh ra cho đời bớt khổ (J4F).

Theo Trang chủ ReactJS:

Keys help React identify which items have changed, are added, or are removed. Keys should be given to the elements a stable identity.


Như vậy, khi chúng ta sửa lại:

<li key={ 0 }>Devnote 0</li>
<li key={ 1 }>Devnote 1</li>
<li key={ 2 }>Devnote 2</li>
<li key={ 3 }>Devnote 3</li>

ReactJS sẽ sử dụng key này trong quá trình reconciliation, so sánh tree cũtree mới thông qua key của từng phần tử, và kết quả là ReactJS sẽ nhận ra được phần tử mới là <li key={ 0 }>Devnote 0</li> được thêm vào phía trước, các phần tử còn lại chỉ là dịch xuống vị trí kế tiếp mà thôi.

FAQs

Nếu không có key thì ứng dụng có chạy không ?

Điều này còn tùy vào từng trường hợp. Thông thường, khi chúng ta không dùng key, ứng dụng sẽ không báo có lỗi mà chỉ có dòng Warning bên phía cửa sổ Console. Song, nếu chúng ta thực hiện các hành động làm thay đổi thứ tự của các phần tử trong mảng như sắp xếp, xóa, etc thì có-thể-sẽ-dẫn-đến các bugs khác.

Hãy cùng nhau điểm qua một ví dụ để hiểu rõ ý này nhé !

Giả sử có một danh sách hobbies:

{ id: 1, hobby: "📷" },
{ id: 2, hobby: "🎹" }

Chúng ta render chúng ra ngoài và cho phép người thể đánh giá mức độ sở thích (likes|| loves) thông qua <select> tag như hình dưới đây:

Lúc này, dù có key hay không thì vẫn có thể rating 📷, 🎹 bình thường và không phát sinh ra bug nào cả. Như đã nói phía trên thì đó là do chúng ta không thực hiện các hành động xóa, sắp xếp... các phần tử này.

Bây giờ, mình thêm một button X vào mỗi phần tử cho phép người dùng xóa phần tử đó:

Được rồi, bây giờ bạn rating 📷 là loves sau đó ấn vào button X để xóa nó đi. Thử quan sát nhé:

After the deletion, the 🤪 emoji appears rated as “Very good”, which is wrong because we haven’t changed its rating at all 😦 Sau khi xóa 📷 đi, 🎹 xuất hiện rating loves dù trước đó là likes và chúng ta chưa hề rating nó !?!

Thật không ổn chút nào khi không dùng key đúng không nào ^^ Xem demo bug này tại đây.

Sử dụng giá trị của keyindex ?

Có thể khi bạn xem trong các Tutorial làm mini-project nhỏ hay một số các demo, giá trị của keyindex (trong map())

Bạn thấy có ổn không 🤔 ? Spoil chút là không nha =))

Bởi vì nếu keyindex thì khi ta thay đổi vị trí của các phần tử trong mảng (xóa, thêm...), index cũng sẽ thay đổi và lại xảy ra vấn đề như ví dụ Devnotes ở mục phía trên.

Best practise

Chính ý nghĩa của key trong quá trình reconciliation, key được recommend nên là các unique value. Kiểu giá trị của key không phải lúc nào cũng phải là number, key có thể là một string, hay thậm chí là emoji kìa ^^

Và để thuận tiện và không cần phải suy nghĩ nhiều, chúng ta có thể set id của mỗi phần tử là giá trị của key.

Cùng sửa lại ví dụ hobbies phía trên:

<li className="hobby-item" key={item.id}>

Bây giờ thử kiểm tra lại bên cửa sổ Console nhé, warning và bug đề cập phía trên không còn nữa rồi 🎉🎉

Kết

Chủ đề này có lẽ không quá mới mẻ với các bạn đã và đang làm việc với ReactJS (kể cả các bạn newbie). Cách fix cũng có rất nhiều trên Stackoverflow và không có gì quá khó khăn.

Song, thông qua bài viết này, mình vẫn muốn chúng ta cùng nhau tìm hiểu rõ: Tại sao lại phải dùng key, dùng thì nó có tác dụng gì ? Có lý do nào đó bên trong ReactJS à? Không dùng thì có 'ổn áp' không ?, etc. Mình cảm ơn các bạn vì đã đọc bài viết này và hy vọng rằng nó có thể giúp ích được cho các bạn ^^ Tặng mình 1 upvote để có thêm động lực cho những bài viết sắp tới nha.

Tham khảo thêm các bài viết về Techniques tại đây ^^

Chúc các bạn cuối tuần vui vẻ ^^


Reference: Personal Blog, Medium, ReactJS Document.