Bài 15.2: JSX và typing components

Chào mừng trở lại với chuỗi bài học về lập trình Web Front-end hiện đại! Sau khi đã làm quen với những khái niệm cơ bản của React, hôm nay chúng ta sẽ cùng nhau đi sâu vào hai trụ cột quan trọng: JSX – ngôn ngữ định nghĩa giao diện của React, và cách sử dụng TypeScript để tăng cường độ tin cậy và khả năng bảo trì cho các component của chúng ta thông qua việc typing.

JSX: Không phải HTML, nhưng giống như HTML

Khi bắt đầu với React, điều đầu tiên bạn thấy sẽ là những đoạn code trông rất giống HTML nằm lẫn trong file JavaScript. Đó chính là JSX (JavaScript XML). JSX không phải là một ngôn ngữ mới hoàn toàn hay một phiên bản của HTML. Nó là một cú pháp mở rộng cho JavaScript, được React sử dụng để mô tả cấu trúc của giao diện người dùng (UI).

Vậy tại sao lại dùng JSX?

  • Dễ đọc và dễ viết: Viết code UI trông giống HTML/XML quen thuộc sẽ trực quan hơn nhiều so với việc gọi các hàm JavaScript lồng nhau.
  • Trực quan hóa cấu trúc UI: Dễ dàng hình dung được cấu trúc cây của các phần tử HTML mà component của bạn sẽ render ra.
  • Tích hợp chặt chẽ với JavaScript: Bạn có thể nhúng các biểu thức JavaScript trực tiếp vào trong cấu trúc UI của mình.

Dưới đây là một ví dụ đơn giản về JSX:

const greeting = <h1>Xin chào, thế giới React!</h1>;

Đoạn code JSX <h1>Xin chào, thế giới React!</h1> không chạy trực tiếp trong trình duyệt. Khi code của bạn được biên dịch (thường là bởi các công cụ như Babel), JSX này sẽ được chuyển đổi thành các lệnh gọi hàm React.createElement() tương ứng.

Ví dụ trên sẽ được biên dịch thành:

const greeting = React.createElement('h1', null, 'Xin chào, thế giới React!');

Bạn có thể thấy, việc viết JSX ngắn gọndễ đọc hơn rất nhiều so với việc gọi trực tiếp React.createElement.

Những quy tắc cơ bản khi sử dụng JSX

Để sử dụng JSX một cách hiệu quả, bạn cần nắm vững một vài quy tắc quan trọng:

  1. Phải có một phần tử gốc duy nhất: Khi trả về nhiều phần tử JSX từ một component hoặc một biểu thức, chúng phải được bọc trong một phần tử cha duy nhất. Điều này có thể là một div, một span, hoặc phổ biến nhất là sử dụng Fragment (<>...</>) để tránh tạo thêm các nút DOM không cần thiết.

    // SAI: Trả về hai phần tử cấp cao
    /*
    return (
        <h1>Tiêu đề</h1>
        <p>Đoạn văn</p>
    );
    */
    
    // ĐÚNG: Bọc trong một div
    return (
        <div>
            <h1>Tiêu đề</h1>
            <p>Đoạn văn</p>
        </div>
    );
    
    // ĐÚNG: Bọc trong Fragment (<>...</>)
    return (
        <>
            <h1>Tiêu đề</h1>
            <p>Đoạn văn</p>
        </>
    );
  2. Sử dụng className thay vì class:class là một từ khóa dành riêng trong JavaScript, bạn phải sử dụng className để thêm lớp CSS cho các phần tử HTML.

    <div className="container">
        <p className="text-center">Nội dung</p>
    </div>
  3. Sử dụng htmlFor thay vì for: Tương tự, for là từ khóa JavaScript, nên bạn dùng htmlFor cho thuộc tính for của thẻ <label>.

    <label htmlFor="input-name">Tên:</label>
    <input type="text" id="input-name" />
  4. Viết thuộc tính theo kiểu camelCase: Hầu hết các thuộc tính HTML được viết dưới dạng camelCase trong JSX (ví dụ: tabIndex thay vì tabindex).

  5. Nhúng biểu thức JavaScript bằng {}: Bạn có thể đặt bất kỳ biểu thức JavaScript hợp lệ nào bên trong cặp dấu ngoặc nhọn {} trong JSX. Biểu thức này sẽ được thực thi và kết quả của nó (nếu là một giá trị renderable như chuỗi, số, hoặc phần tử JSX khác) sẽ được hiển thị trên UI.

    const name = 'FullhouseDev';
    const isLoggedIn = true;
    
    function formatName(user) {
        return user.firstName + ' ' + user.lastName;
    }
    
    const user = { firstName: 'John', lastName: 'Doe' };
    
    return (
        <>
            <h1>Xin chào, {name}!</h1> {/* Nhúng biến */}
            <p>Tổng của 2 + 3 là: {2 + 3}</p> {/* Nhúng biểu thức toán học */}
            <p>Tên đầy đủ: {formatName(user)}</p> {/* Nhúng kết quả hàm */}
    
            {/* Nhúng điều kiện sử dụng toán tử logic && */}
            {isLoggedIn && <button>Đăng xuất</button>}
    
            {/* Nhúng điều kiện sử dụng toán tử ba ngôi */}
            {isLoggedIn ? <p>Chào mừng trở lại!</p> : <p>Hãy đăng nhập.</p>}
        </>
    );
  6. Render danh sách với map: Để hiển thị một danh sách các phần tử dựa trên dữ liệu từ một mảng, bạn thường sử dụng phương thức map() của mảng và trả về một phần tử JSX cho mỗi mục trong mảng. Lưu ý quan trọng: Khi render danh sách, hãy luôn thêm thuộc tính key với một giá trị duy nhất cho mỗi phần tử trong danh sách. key giúp React theo dõi các mục trong danh sách, cải thiện hiệu suất và tránh các lỗi không mong muốn khi danh sách thay đổi.

    const items = [
        { id: 1, name: 'Apple' },
        { id: 2, name: 'Banana' },
        { id: 3, name: 'Orange' }
    ];
    
    return (
        <ul>
            {items.map(item => (
                <li key={item.id}>
                    {item.name}
                </li>
            ))}
        </ul>
    );

    Trong ví dụ này, key={item.id} gán một khóa duy nhất cho mỗi mục <li> dựa trên thuộc tính id của đối tượng.

Typing Components với TypeScript

JSX giúp chúng ta dễ dàng mô tả giao diện. Tuy nhiên, khi ứng dụng của bạn lớn dần, việc đảm bảo rằng các component nhận đúng loại dữ liệu (props) và trả về đúng định dạng là vô cùng quan trọng để tránh lỗi và làm cho code dễ hiểu hơn. Đây là lúc TypeScript phát huy sức mạnh, đặc biệt là trong việc typing components.

Tại sao lại typing components?

  • Phát hiện lỗi sớm: TypeScript kiểm tra kiểu dữ liệu trước khi bạn chạy ứng dụng (trong quá trình biên dịch), giúp bắt được nhiều lỗi phổ biến liên quan đến kiểu dữ liệu.
  • Tăng cường khả năng bảo trì: Code có kiểu rõ ràng dễ đọc, dễ hiểu và dễ thay đổi hơn cho cả bạn và những người khác làm việc cùng dự án.
  • Cải thiện trải nghiệm phát triển: Các IDE hiện đại có thể cung cấp tính năng tự động hoàn thành (autocompletion) và gợi ý code thông minh dựa trên thông tin kiểu.
  • Tài liệu sống: Khai báo kiểu cho props hoạt động như một dạng tài liệu cho biết component đó mong đợi những dữ liệu gì.

Hãy xem cách chúng ta có thể typing các component hàm (Functional Components), vốn là cách phổ biến nhất để viết component trong React hiện đại.

1. Typing Props bằng Interface hoặc Type

Cách phổ biến nhất để typing props cho một component là định nghĩa một interface hoặc type mô tả cấu trúc và kiểu dữ liệu của các props mà component đó nhận vào.

// Định nghĩa interface cho props của component Greeting
interface GreetingProps {
    name: string; // name là một chuỗi bắt buộc
    message?: string; // message là một chuỗi tùy chọn (dùng dấu ?)
    age: number; // age là một số bắt buộc
    isLoggedIn: boolean; // isLoggedIn là boolean
    userData: { id: number; username: string }; // userData là một object với cấu trúc cụ thể
    tags: string[]; // tags là một mảng các chuỗi
    onButtonClick: (event: React.MouseEvent<HTMLButtonElement>) => void; // onButtonClick là một hàm
}

// Khai báo component hàm và gán kiểu props
// Cách 1: Sử dụng React.FC (FunctionComponent)
// const Greeting: React.FC<GreetingProps> = ({ name, message, age, isLoggedIn, userData, tags, onButtonClick }) => {
//     return (
//         <div>
//             <h2>Hello, {name}!</h2>
//             {message && <p>{message}</p>}
//             <p>Age: {age}</p>
//             <p>Status: {isLoggedIn ? 'Online' : 'Offline'}</p>
//             <p>User: {userData.username} ({userData.id})</p>
//             <p>Tags: {tags.join(', ')}</p>
//             <button onClick={onButtonClick}>Click Me</button>
//         </div>
//     );
// };

// Cách 2: Gán kiểu trực tiếp cho tham số props (thường được ưa chuộng hơn hiện nay)
const Greeting = ({ name, message, age, isLoggedIn, userData, tags, onButtonClick }: GreetingProps) => {
    return (
        <div>
            <h2>Hello, {name}!</h2>
            {/* Chỉ hiển thị message nếu có */}
            {message && <p>{message}</p>}
            <p>Age: {age}</p>
            <p>Status: {isLoggedIn ? 'Online' : 'Offline'}</p>
            <p>User: {userData.username} ({userData.id})</p>
            <p>Tags: {tags.join(', ')}</p>
            <button onClick={onButtonClick}>Click Me</button>
        </div>
    );
};

// Cách sử dụng component (TypeScript sẽ kiểm tra bạn có truyền đúng props không)
function App() {
    const handleButtonClick = () => {
        alert('Button clicked!');
    };

    const userInfo = { id: 123, username: 'coder1' };
    const userTags = ['typescript', 'react', 'web'];

    return (
        <Greeting
            name="Alice" // name (string, bắt buộc) -> OK
            age={30} // age (number, bắt buộc) -> OK
            isLoggedIn={true} // isLoggedIn (boolean, bắt buộc) -> OK
            userData={userInfo} // userData (object, bắt buộc) -> OK
            tags={userTags} // tags (string[], bắt buộc) -> OK
            onButtonClick={handleButtonClick} // onButtonClick (hàm, bắt buộc) -> OK
            // Không truyền message vì nó là tùy chọn
        />
        // Nếu bạn truyền sai kiểu hoặc thiếu prop bắt buộc, TypeScript sẽ báo lỗi ngay lúc biên dịch!
        /*
        <Greeting name="Bob" age="twenty" /> // Lỗi: age phải là number
        <Greeting name="Charlie" /> // Lỗi: thiếu age, isLoggedIn, userData, tags, onButtonClick
        */
    );
}

Trong ví dụ trên:

  • Chúng ta định nghĩa một interface GreetingProps để mô tả hình dạng của đối tượng props.
  • Mỗi thuộc tính trong interface có tên và kiểu dữ liệu cụ thể (string, number, boolean, { ... }, string[], (event: React.MouseEvent<HTMLButtonElement>) => void).
  • Dấu hỏi ? sau tên thuộc tính (message?: string) chỉ ra rằng prop đó là tùy chọn (optional). Nếu không có dấu hỏi, prop đó là bắt buộc.
  • Chúng ta gán kiểu GreetingProps cho tham số của component hàm Greeting. Bằng cách này, TypeScript biết được component này mong đợi những props nào và có kiểu dữ liệu ra sao.
  • Khi sử dụng <Greeting> trong component App, TypeScript sẽ kiểm tra xem bạn có truyền đầy đủ các props bắt buộc với đúng kiểu dữ liệu đã khai báo hay không. Điều này ngăn chặn lỗi undefined hoặc lỗi kiểu dữ liệu tại runtime.

2. Typing Children Prop

Nếu component của bạn có thể nhận và hiển thị các phần tử con (child elements) được truyền vào giữa cặp thẻ mở/đóng của nó, bạn cần thêm children vào interface props của mình. Kiểu dữ liệu phổ biến cho childrenReact.ReactNode. React.ReactNode là một kiểu kết hợp linh hoạt, bao gồm:

  • Các phần tử React (JSX).
  • Chuỗi (string).
  • Số (number).
  • Booleans (true/false - không render nhưng được chấp nhận).
  • Null hoặc Undefined (không render).
  • Mảng các React.ReactNode.
interface ContainerProps {
    title: string;
    // children có kiểu React.ReactNode
    children: React.ReactNode;
}

const Container = ({ title, children }: ContainerProps) => {
    return (
        <div style={{ border: '1px solid black', padding: '10px' }}>
            <h3>{title}</h3>
            {/* Render children được truyền vào */}
            {children}
        </div>
    );
};

// Cách sử dụng component Container với children
function AnotherApp() {
    return (
        <Container title="Đây là Tiêu đề Container">
            {/* Các phần tử bên trong cặp thẻ <Container> là children */}
            <p>Đây  nội dung nằm *trong* Container.</p>
            <ul>
                <li>Mục 1</li>
                <li>Mục 2</li>
            </ul>
            {'Một đoạn text bất kỳ'}
            {12345}
        </Container>
    );
}

Trong ví dụ này, component Container có prop children với kiểu React.ReactNode, cho phép nó nhận vào bất kỳ nội dung React hợp lệ nào (đoạn văn, danh sách, text, số, v.v.) và hiển thị chúng tại vị trí {children} trong JSX.

Comments

There are no comments at the moment.