Fragment trong React cho phép một component trả về nhiều phần tử mà không phải bọc chúng trong một <div> thừa. Viết <> trước phần tử đầu tiên và </> sau phần tử cuối cùng, React sẽ đặt các phần tử thẳng vào phần tử cha, không có nút riêng nào trên trang.
Mỗi Greeting trả về một tiêu đề và một đoạn văn đặt cạnh nhau. Xóa hai dòng <> và </> thì phần xem trước sẽ hiện lỗi build, vì lúc này hàm đang cố trả về hai giá trị.
Vì sao component cần một phần tử gốc
JSX là một cách dễ đọc hơn để viết lời gọi hàm. <h2>Hi</h2> được biên dịch gần giống jsx('h2', { children: 'Hi' }), tức là một biểu thức. Câu lệnh return chỉ trả về được một giá trị, nên hai thẻ ngang hàng không có gì bọc quanh cũng giống như viết return a b: trình biên dịch dừng lại với lỗi "Adjacent JSX elements must be wrapped in an enclosing tag".
Trước khi có fragment, cách sửa là một <div> bọc ngoài. Cách này chạy được, nhưng div là một phần tử thật. Nó xuất hiện trong DOM, tham gia vào bố cục CSS, và một số phần tử cha hoàn toàn không chấp nhận nó làm con. Fragment thỏa mãn quy tắc một phần tử gốc rồi biến mất.
Bạn có thể thấy rõ là nó không thêm gì. Ví dụ dưới đây render cùng hai mục qua một fragment và qua một div, rồi đếm số phần tử con trực tiếp của mỗi container.
Console in ra 2 cho fragment, tức là hai thẻ span, và 1 cho div, vì div bọc cả hai. Trong một container flex hoặc grid, khác biệt này quyết định các span được sắp xếp như từng mục riêng hay bị gộp làm một.
Cú pháp ngắn và component Fragment
Có hai cách viết fragment, và cả hai được biên dịch thành cùng một thứ:
import { Fragment } from 'react';
// Short syntax, no import needed
<>
<dt>React</dt>
<dd>A library for building user interfaces</dd>
</>
// Long form
<Fragment>
<dt>React</dt>
<dd>A library for building user interfaces</dd>
</Fragment>
Hầu như ở đâu cũng nên dùng <>...</>. Chỉ dùng <Fragment> trong một trường hợp: khi fragment cần key. Cú pháp ngắn không nhận thuộc tính, nên <key={id}> không phải JSX hợp lệ. Bạn cũng có thể gặp <React.Fragment> trong code cũ; đó chính là component này, được truy cập qua import mặc định React.
Fragment có key trong danh sách
Khi render một danh sách bằng map, mỗi mục cần một key để React theo dõi nó giữa các lần render (trang danh sách và key giải thích lý do). Nếu mỗi mục là một nhóm phần tử thay vì một phần tử, key sẽ đặt trên fragment, nên bạn cần dạng đầy đủ.
Danh sách mô tả là trường hợp điển hình: mỗi mục gồm một <dt> và một <dd>, và <dl> nên chứa trực tiếp các phần tử đó.
Click Reverse và các cặp đổi chỗ cho nhau như những khối trọn vẹn, vì mỗi cặp có key riêng. Một <div> bọc quanh mỗi cặp cũng nhận được key, và vẫn là HTML hợp lệ bên trong dl, nhưng nó thêm một cấp thứ ba vào markup mà fragment giữ được gọn.
Fragment bên trong bảng
Bảng là nơi một lớp bọc thừa gây hại nhiều nhất. <tr> cần các ô <td> và <th> làm phần tử con trực tiếp. Nếu một component trả về nhiều ô mà bọc chúng trong <div>, trình duyệt sẽ coi div là một ô lạ, và các ô bên trong không còn thẳng hàng với tiêu đề.
Ở hàng dùng fragment, giá nằm dưới Price và số lượng tồn kho nằm dưới Stock. Ở hàng dùng div, cả hai ô bị dồn vào cột Price và cột Stock bị trống. Ở môi trường phát triển, React còn ghi một cảnh báo rằng <div> không thể là con của <tr>; phần xem trước chạy như bản build production nên bỏ qua bước kiểm tra đó, và bố cục bị vỡ chính là thứ bạn thấy ở đây. Khi render phía máy chủ, cùng lỗi này còn có thể gây lỗi hydration, vì trình phân tích HTML của trình duyệt sẽ di chuyển thẻ div đặt sai chỗ.
Quy tắc này cũng áp dụng cho <ul> và <ol>, vốn nên chứa các phần tử <li>, và cho <select>, vốn nên chứa các phần tử <option>. Bất cứ khi nào một component trả về những phần nằm trực tiếp bên trong một phần tử cha như vậy, hãy trả về một fragment.
Fragment hay mảng
Component cũng có thể trả về một mảng phần tử, và React render chúng theo thứ tự giống như fragment. Mỗi phần tử trong mảng trả về cần key riêng, giống như kết quả của map, vì React coi mảng đó là một danh sách:
function Cells() {
return [<td key="price">$4</td>, <td key="stock">In stock</td>];
}
Cách này chạy được nhưng rườm rà hơn: dấu phẩy giữa các phần tử, key trên từng phần tử, và một dấu ngoặc vuông dễ bị bỏ sót khi markup dài ra. Hãy dùng fragment cho một nhóm phần tử cố định, và để dành mảng cho đúng vai trò tự nhiên của nó: kết quả của việc map qua dữ liệu.
Những gì fragment không làm được
Vì fragment không render ra phần tử nào, nó không có chỗ để gắn class, style, trình xử lý sự kiện hay ref. <Fragment className="row"> không có tác dụng gì, và prop duy nhất React đọc từ fragment là key. Khi cần tạo style cho cả nhóm hoặc lắng nghe click trên nó, rốt cuộc bạn vẫn cần một phần tử thật, và điều đó hoàn toàn bình thường: hãy chọn phần tử hợp lệ ở vị trí đó, như <tbody> để gom các hàng hoặc <li> để chứa một mục danh sách.
Fragment dùng được ở bất cứ đâu một phần tử dùng được, kể cả bên trong render có điều kiện: {show && <>...</>} hiện hoặc ẩn cả nhóm cùng lúc.
Câu hỏi thường gặp
Fragment trong React là gì?
Fragment là một lớp bọc cho phép component trả về nhiều phần tử như một giá trị duy nhất mà không thêm nút DOM nào. <><h1 /><p /></> đặt h1 và p thẳng vào phần tử cha.
<> và <Fragment> khác nhau thế nào?
Chúng tạo ra cùng một kết quả. <>...</> là cú pháp ngắn. <Fragment> là dạng đầy đủ bạn import từ react, và là dạng duy nhất nhận được key, thứ bạn cần khi render fragment trong một danh sách.
Vì sao nên dùng Fragment thay vì div?
div là một phần tử thật. Nó có thể làm hỏng bố cục của những phần tử cha cần phần tử con trực tiếp (container flex và grid, ul, table, tr) và làm DOM rối thêm. Fragment không thêm gì cả.
Fragment có nhận className hay onClick được không?
Không. Fragment không render ra phần tử nào, nên không có chỗ để gắn class, style hay trình xử lý sự kiện. Prop duy nhất nó nhận là key. Nếu cần những thứ đó, hãy dùng một phần tử thật.
Vì sao tôi gặp lỗi "Adjacent JSX elements must be wrapped in an enclosing tag"?
Component đã trả về hai phần tử ngang hàng mà không có gì bọc quanh. JSX được biên dịch thành một biểu thức duy nhất, nên hãy bọc các phần tử ngang hàng trong <>...</>.