JSX được compiler dịch thành lời gọi hàm tạo element — React.createElement(type, props, ...children) ở bản cũ, và _jsx() từ react/jsx-runtime kể từ React 17.
// Bạn viết
<h1 className="title">Xin chào</h1>
// Babel dịch (JSX transform cũ)
React.createElement('h1', { className: 'title' }, 'Xin chào')
// Babel dịch (JSX transform mới, React 17+)
import { jsx as _jsx } from 'react/jsx-runtime'
_jsx('h1', { className: 'title', children: 'Xin chào' })Kết quả trả về không phải DOM node, mà là một object mô tả thuần: { type: 'h1', props: { className: 'title', children: 'Xin chào' } }. React đọc object này rồi mới quyết định đụng vào DOM thật cái gì. Đây chính là chỗ Virtual DOM bắt đầu.
Thẻ lồng nhau thành đối số children lồng nhau, nên <div><span>a</span></div> dịch thành createElement('div', null, createElement('span', null, 'a')). Vì đây là lời gọi hàm, đối số phải là biểu thức — đó là lý do gốc khiến if và for không đặt được trong ngoặc nhọn JSX.
Chữ hoa/thường quyết định React hiểu thẻ là gì: <button> (chữ thường) dịch thành chuỗi 'button' cho thẻ HTML, còn <Button> (chữ hoa) dịch thành tham chiếu tới biến Button. Viết nhầm chữ thường cho component của mình là lỗi khiến React render ra thẻ HTML lạ thay vì báo lỗi rõ ràng.
Lưu ý: JSX transform mới là lý do từ React 17 trở đi không cần import React chỉ để viết JSX. Nhưng vẫn cần import React khi dùng API của nó (useState import từ react, React.memo...).