17 Câu Hỏi Phỏng Vấn React Bắt Buộc Phải Biết Cho Developer Năm 2025

Trong thế giới phát triển web năng động năm 2025, React vẫn là một trong những kỹ năng được săn đón hàng đầu đối với các nhà phát triển Frontend. Nếu bạn đang chuẩn bị cho một buổi phỏng vấn với vị trí kỹ sư React, việc nắm vững các nguyên lý cốt lõi, kiến thức chuyên sâu về hiệu năng và các thực hành tốt nhất là chìa khóa để ghi điểm.

Bài viết này tổng hợp 17 câu hỏi phỏng vấn React quan trọng, bao quát từ những khái niệm cơ bản đến các kỹ thuật tối ưu hóa hiệu suất nâng cao. Đây là những kiến thức nền tảng giúp bạn tự tin trả lời và vượt qua buổi phỏng vấn sắp tới.

Hãy cùng đi sâu vào chi tiết!

1. Virtual DOM của React là gì? Nó khác Real DOM và Shadow DOM như thế nào?

Virtual DOM (DOM Ảo) là một khái niệm trong React, nơi một bản sao ảo của Real DOM (DOM Thật) được lưu trữ trong bộ nhớ. React sử dụng một thư viện như ReactDOM để đồng bộ hóa bản sao ảo này với DOM thật chỉ khi cần thiết.

Virtual DOM là một đối tượng JavaScript biểu diễn cấu trúc cây của DOM thật. Việc cập nhật DOM thật là một thao tác tốn kém nhất trong phát triển giao diện người dùng. Virtual DOM giúp tối ưu hóa quy trình này bằng cách so sánh bản sao ảo trước và sau khi dữ liệu thay đổi, chỉ xác định những phần nào của giao diện cần cập nhật, sau đó thực hiện cập nhật tối thiểu lên DOM thật. Điều này cải thiện đáng kể hiệu suất.

Virtual DOM là một khái niệm khác biệt hoàn toàn so với Real DOMShadow DOM.

  • Real DOM: Là cấu trúc cây thực tế của tài liệu HTML mà trình duyệt sử dụng để theo dõi nội dung của trang web. Đây là cách các thẻ HTML được biểu diễn trong bộ nhớ trình duyệt.
  • Shadow DOM: Là một tính năng của trình duyệt cho phép tạo ra các cấu trúc DOM độc lập, biệt lập (isolated) cho các component web. Nó giúp đóng gói HTML, CSS và hành vi của component, ngăn chặn xung đột tên lớp CSS hoặc các vấn đề khác từ bên ngoài component. Shadow DOM thường được sử dụng để xây dựng các Web Components có thể tái sử dụng.

Tóm lại, Virtual DOM là công cụ tối ưu hóa hiệu suất rendering của React, trong khi Real DOM là cấu trúc DOM thực tế, và Shadow DOM là kỹ thuật đóng gói component của trình duyệt.

2. Có những loại Component nào trong React? Khi nào sử dụng loại nào?

Trong React, có hai loại component chính:

  1. Class Components (Component Lớp)
  2. Functional Components (Component Hàm)

Trước đây, Class Components là cách duy nhất để tạo các component có state (trạng thái) và lifecycle methods (các phương thức vòng đời). Functional Components lúc đó chỉ được sử dụng như “presentational components” (component trình bày) hoặc được gọi là component “câm” (dumb) vì chúng chỉ nhận props và hiển thị giao diện mà không có logic phức tạp hay quản lý trạng thái nội bộ.

Tuy nhiên, với sự ra mắt của React 16.8 và giới thiệu React Hooks, Functional Components giờ đây có thể có state (với `useState`) và xử lý các logic liên quan đến vòng đời component (với `useEffect`). Điều này đã biến Functional Components trở thành cách được ưu tiên để viết component trong React.

Functional Components thường đơn giản hơn, dễ đọc hơn và có hiệu suất tốt hơn so với Class Components do ít mã boilerplate và overhead. Do đó, khuyến nghị sử dụng Functional Components bất cứ khi nào có thể.

Tuy nhiên, một số lifecycle methods vẫn chỉ có sẵn trong Class Components, ví dụ như `componentDidCatch` để tạo Error Boundaries (vùng xử lý lỗi). Trong những trường hợp cụ thể như vậy, bạn có thể cần sử dụng Class Components.

3. Tại sao cần `key` trong React? Có thể sử dụng `key` mà không phải với List không?

Trong React, thuộc tính `key` được sử dụng để giúp React nhận diện các phần tử Virtual DOM duy nhất tương ứng với dữ liệu điều khiển giao diện. Khi một danh sách các phần tử được render, React sử dụng `key` để xác định item nào đã thay đổi, thêm mới hoặc bị xóa. Điều này cho phép React tái sử dụng (recycle) các phần tử DOM hiện có thay vì render lại toàn bộ danh sách, từ đó cung cấp một cải thiện đáng kể về hiệu suất, đặc biệt khi làm việc với các danh sách lớn hoặc thường xuyên thay đổi.

Ví dụ, khi render một danh sách:

const Todos = ({ todos }) => {
  return (
    <div>
      {todos.map((todo) => (
        // Thiếu key: React sẽ render lại toàn bộ li khi todos thay đổi
        <li>{todo.text}</li>
      ))}
    </div>
  );
};

Bằng cách thêm `key`:

const Todos = ({ todos }) => {
  return (
    <div>
      {todos.map((todo) => (
        // Có key: React có thể nhận diện và chỉ cập nhật các li cần thiết
        <li key={todo.id}>{todo.text}</li>
      ))}
    </div>
  );
};

React sẽ sử dụng `key` (`todo.id`) để theo dõi từng `

  • `. Khi danh sách `todos` thay đổi, React sẽ so sánh các `key` để tìm ra sự khác biệt và cập nhật DOM hiệu quả hơn.

    Có thể sử dụng `key` mà không phải với List không?

    Về lý thuyết, `key` có thể được sử dụng với bất kỳ phần tử nào trong React, không nhất thiết phải là trong danh sách. Tuy nhiên, mục đích chính của `key` là tối ưu hóa việc render các danh sách động. Một số trường hợp sử dụng `key` ngoài danh sách (như ví dụ trong code gốc để buộc re-render) là không chuẩn và thường được coi là anti-pattern. Cách đúng đắn hơn để quản lý re-render là thông qua quản lý state hoặc props. Sử dụng `key` để buộc re-render có thể dẫn đến các vấn đề hiệu suất nghiêm trọng nếu không hiểu rõ cách React hoạt động.

    4. Phân biệt Controlled Input và Uncontrolled Input trong React

    Trong React, việc quản lý dữ liệu của các phần tử form (như `input`, `textarea`, `select`) có thể được thực hiện theo hai cách chính:

    • Controlled Components (Component được kiểm soát): Dữ liệu form được quản lý hoàn toàn bởi React State. Giá trị của phần tử form luôn phản ánh giá trị của state. Mỗi khi người dùng nhập liệu, state sẽ được cập nhật thông qua một sự kiện (ví dụ: `onChange`), và giá trị hiển thị trên phần tử form sẽ được thiết lập lại dựa trên state đã cập nhật (`value={stateValue}`).
    • Uncontrolled Components (Component không được kiểm soát): Dữ liệu form được quản lý trực tiếp bởi DOM. Giá trị hiện tại của phần tử form được truy cập thông qua tham chiếu DOM (sử dụng `useRef` hoặc refs truyền thống trong Class Components). React không “kiểm soát” giá trị hiển thị của phần tử form.

    Ưu điểm và trường hợp sử dụng:

    • Controlled Components:
      • Được ưu tiên trong hầu hết các trường hợp.
      • Cung cấp một nguồn “chân lý” duy nhất cho dữ liệu form (chính là React State).
      • Giúp việc xử lý form dễ dàng hơn: validation (kiểm tra tính hợp lệ), thao tác với dữ liệu (format, lọc), điều kiện disabled button dựa trên input, v.v.
      • Mọi thay đổi đều được React theo dõi thông qua state.
    • Uncontrolled Components:
      • Đôi khi hữu ích cho các form đơn giản hoặc khi tích hợp với thư viện DOM không phải React.
      • Thao tác nhanh hơn cho một số trường hợp rất đặc biệt (ví dụ: upload file).
      • Cần truy cập trực tiếp đến phần tử DOM để lấy hoặc thiết lập giá trị.

    Ví dụ code:

    import React, { useState, useRef } from 'react';
    
    // Controlled Component
    const ControlledInputForm = () => {
      const [value, setValue] = useState("");
    
      const handleChange = (e) => {
        setValue(e.target.value);
      };
    
      const handleSubmit = (e) => {
        e.preventDefault();
        console.log("Giá trị từ Controlled Input:", value);
        // Có thể thực hiện validation, submit... tại đây
      };
    
      return (
        <form onSubmit={handleSubmit}>
          <input type="text" value={value} onChange={handleChange} />
          <button type="submit">Gửi (Controlled)</button>
        </form>
      );
    };
    
    // Uncontrolled Component
    const UncontrolledInputForm = () => {
      const inputRef = useRef(null);
    
      const handleSubmit = (e) => {
        e.preventDefault();
        console.log("Giá trị từ Uncontrolled Input:", inputRef.current.value);
        // Lấy giá trị trực tiếp từ DOM element thông qua ref
      };
    
      return (
        <form onSubmit={handleSubmit}>
          <input type="text" ref={inputRef} />
          <button type="submit">Gửi (Uncontrolled)</button>
        </form>
      );
    };
    
    // Cách sử dụng
    const App = () => (
      <div>
        <h3>Ví dụ Controlled Component</h3>
        <ControlledInputForm />
        <h3>Ví dụ Uncontrolled Component</h3>
        <UncontrolledInputForm />
      </div>
    );
    

    Trong ví dụ Controlled, giá trị của input luôn được đồng bộ với state `value`. Trong ví dụ Uncontrolled, giá trị được lấy trực tiếp từ DOM element khi submit.

    5. Tại sao chúng ta cần Transpile mã JSX?

    JSX (JavaScript XML) là một cú pháp mở rộng cho JavaScript, được sử dụng phổ biến trong React để mô tả cấu trúc giao diện người dùng một cách trực quan, gần giống với HTML. Ví dụ:

    const Greeting = ({ name }) => <h1 className="greeting">Xin chào, {name}!</h1>;
    

    Tuy nhiên, các trình duyệt web không hiểu trực tiếp cú pháp JSX. Trình duyệt chỉ có thể hiểu và thực thi mã JavaScript tiêu chuẩn.

    Do đó, chúng ta cần một quy trình gọi là Transpilation (chuyển đổi mã) để biến đổi mã JSX thành mã JavaScript mà trình duyệt có thể xử lý. Công cụ phổ biến nhất thực hiện công việc này là Babel.

    Khi Babel xử lý đoạn mã JSX ở trên, nó sẽ chuyển đổi nó thành các lệnh gọi hàm JavaScript, thường là `React.createElement` (hoặc các hàm tương đương trong các phiên bản React mới hơn):

    import { createElement } from "react";
    
    const Greeting = ({ name }) => {
      return createElement(
        "h1",
        { className: "greeting" },
        "Xin chào, ",
        name,
        "!"
      );
    };
    

    Đây chính là lý do tại sao chúng ta cần transpile mã JSX: nó cho phép nhà phát triển viết code React một cách dễ đọc và trực quan hơn bằng cú pháp JSX, trong khi vẫn đảm bảo rằng mã cuối cùng được thực thi bởi trình duyệt là JavaScript hợp lệ.

    6. JSX ngăn chặn các cuộc tấn công Injection Attack như thế nào?

    JSX cung cấp một lớp bảo vệ chống lại các loại tấn công Injection Attack, đặc biệt là Cross-Site Scripting (XSS), một cách mặc định. Điều này là do cách React xử lý nội dung được nhúng trong JSX.

    Khi bạn nhúng một biến vào trong JSX bằng dấu ngoặc nhọn `{}`:

    const MyComponent = ({ userInput }) => {
      // Giả sử userInput chứa mã độc HTML/Script, ví dụ: "<script>alert('XSS')</script>"
      return <div>{userInput}</div>;
    };
    

    React sẽ tự động thoát (escape) bất kỳ giá trị nào được truyền vào. Điều này có nghĩa là các ký tự đặc biệt trong HTML, như `<` và `>`, sẽ được chuyển đổi thành các entity HTML tương ứng (`<` và `>`). Do đó, thay vì trình duyệt diễn giải nội dung là mã HTML hoặc script thực thi, nó sẽ chỉ hiển thị nội dung đó dưới dạng văn bản thuần túy trên màn hình.

    Ví dụ, nếu `userInput` là ``, output cuối cùng trong DOM sẽ là:

    <div>&lt;script&gt;alert('XSS')&lt;/script&gt;</div>
    

    Trình duyệt sẽ hiển thị chính xác chuỗi ký tự `` chứ không thực thi mã script.

    Lưu ý quan trọng: React cung cấp một thuộc tính gọi là `dangerouslySetInnerHTML` cho phép bạn chèn trực tiếp mã HTML vào phần tử DOM. Tên của thuộc tính đã nói lên tất cả – nó cực kỳ nguy hiểm nếu bạn không chắc chắn về nguồn gốc của nội dung bạn đang chèn. Chỉ sử dụng nó khi thực sự cần thiết và luôn luôn làm sạch (sanitize) nội dung từ người dùng trước khi chèn bằng cách này để loại bỏ mọi mã độc tiềm ẩn.

    const MyComponentDangerous = ({ rawHtml }) => {
      // Cực kỳ cẩn trọng khi sử dụng!
      return <div dangerouslySetInnerHTML={{ __html: rawHtml }} />;
    };
    

    7. Làm thế nào để thêm Styling vào React Components?

    Có nhiều phương pháp phổ biến để thêm CSS styling vào các component React:

    a) Sử dụng các tệp CSS thông thường

    Đây là phương pháp đơn giản và quen thuộc nhất. Bạn viết CSS trong các tệp `.css` riêng biệt và import chúng vào component. Đây là cách mặc định khi sử dụng các công cụ như Create React App.

    /* Button.css */
    .button {
      background-color: blue;
      color: white;
      padding: 10px 20px;
      border-radius: 5px;
    }
    
    // Button.js
    import "./Button.css";
    
    const Button = ({ children }) => {
      return <button className="button">{children}</button>;
    };
    

    Ưu điểm: Quen thuộc, sử dụng toàn bộ các tính năng CSS. Nhược điểm: Dễ bị xung đột tên lớp (class name collisions) trong các dự án lớn.

    b) Inline CSS (CSS nội tuyến)

    Bạn có thể áp dụng style trực tiếp cho các phần tử bằng cách sử dụng thuộc tính `style`, nhận một đối tượng JavaScript.

    const Button = ({ children }) => {
      const buttonStyle = {
        backgroundColor: "blue",
        color: "white",
        padding: "10px 20px",
        borderRadius: "5px",
        border: "none",
        cursor: "pointer"
      };
      return (
        <button style={buttonStyle}>
          {children}
        </button>
      );
    };
    

    Ưu điểm: Style được đóng gói hoàn toàn cho từng phần tử, tránh xung đột. Nhược điểm: Không thể sử dụng pseudo-classes (`:hover`, `:active`), pseudo-elements (`::before`, `::after`), media queries một cách trực tiếp. Đối tượng style có thể trở nên cồng kềnh.

    c) CSS-in-JS Libraries (Các thư viện CSS-in-JS)

    Các thư viện như Styled Components, Emotion cho phép bạn viết CSS trực tiếp trong mã JavaScript/TypeScript, thường sử dụng cú pháp tagged template literals. Chúng tạo ra các component React với style kèm theo.

    import styled from "styled-components";
    
    const StyledButton = styled.button`
      background-color: blue;
      color: white;
      padding: 10px 20px;
      border-radius: 5px;
      border: none;
      cursor: pointer;
    
      &:hover {
        opacity: 0.9;
      }
    `;
    
    const App = () => {
      return <StyledButton>Click me</StyledButton>;
    };
    

    Ưu điểm: Style được tự động phạm vi hóa (scoped) cho từng component, hỗ trợ logic dynamic style dựa trên props, tổ chức style theo component. Nhược điểm: Cần thêm thư viện, có thể có overhead về runtime.

    d) CSS Modules

    CSS Modules giải quyết vấn đề xung đột tên lớp bằng cách tự động tạo ra các tên lớp CSS duy nhất cho mỗi tệp. Bạn viết CSS thông thường trong tệp `.module.css` và import nó dưới dạng một đối tượng JavaScript.

    /* Button.module.css */
    .button {
      background-color: blue;
      color: white;
      padding: 10px 20px;
      border-radius: 5px;
      border: none;
      cursor: pointer;
    }
    
    // Button.js
    import styles from "./Button.module.css";
    
    const Button = ({ children }) => {
      // styles.button sẽ là một tên lớp duy nhất được tạo tự động
      return <button className={styles.button}>{children}</button>;
    };
    

    Ưu điểm: Tránh xung đột tên lớp hiệu quả, vẫn sử dụng cú pháp CSS quen thuộc, dễ dàng chia sẻ style. Nhược điểm: Cần cấu hình build tool (thường được hỗ trợ sẵn trong các boilerplate như Create React App, Next.js).

    Lựa chọn phương pháp styling phụ thuộc vào quy mô dự án, sở thích cá nhân và yêu cầu cụ thể.

    8. Synthetic Events trong React là gì?

    Synthetic Events (Sự kiện tổng hợp) trong React là một lớp bọc (wrapper) xung quanh các sự kiện gốc (native events) của trình duyệt. React tạo ra hệ thống sự kiện riêng này để đảm bảo rằng các sự kiện hoạt động nhất quán trên các trình duyệt khác nhau (ví dụ: IE, Chrome, Firefox có thể có các triển khai sự kiện gốc khác nhau). Synthetic Event system cung cấp một API chuẩn cho các sự kiện.

    Khi một sự kiện xảy ra (ví dụ: click chuột, gõ phím), React không sử dụng trực tiếp sự kiện gốc của trình duyệt. Thay vào đó, nó tạo ra một đối tượng Synthetic Event, có giao diện tương tự như sự kiện gốc nhưng được chuẩn hóa. Đối tượng này được truyền làm đối số cho các hàm xử lý sự kiện mà bạn định nghĩa (như `onClick`, `onChange`, `onSubmit`).

    Đối tượng Synthetic Event có các thuộc tính và phương thức tương tự như sự kiện gốc (ví dụ: `target`, `preventDefault()`, `stopPropagation()`), nhưng chúng được đảm bảo hoạt động giống nhau bất kể trình duyệt.

    Ví dụ:

    const MyLink = () => {
      const handleClick = (e) => {
        // 'e' ở đây là một Synthetic Event
        e.preventDefault(); // Ngăn chặn hành vi mặc định của thẻ 'a' (chuyển hướng)
        console.log("Đã click vào liên kết, nhưng không chuyển trang.");
      };
    
      return (
        <a href="https://react.dev" onClick={handleClick}>
          Liên kết React
        </a>
      );
    };
    

    Trong ví dụ trên, `e` là một đối tượng Synthetic Event. Gọi `e.preventDefault()` hoạt động nhất quán trên mọi trình duyệt được hỗ trợ bởi React.

    Synthetic Event system cũng đóng góp vào hiệu suất của React bằng cách sử dụng kỹ thuật event delegation (ủy quyền sự kiện): React chỉ đính kèm một trình xử lý sự kiện duy nhất ở cấp root của ứng dụng, và các sự kiện từ các phần tử con sẽ “nổi bọt” lên đó để được xử lý.

    9. Strict Mode trong React là gì?

    `<StrictMode />` là một công cụ hữu ích trong React, được thiết kế để giúp nhà phát triển tìm và khắc phục các vấn đề tiềm ẩn trong ứng dụng của họ trong quá trình phát triển. Nó không hiển thị bất kỳ giao diện người dùng nào, chỉ kích hoạt các kiểm tra và cảnh báo bổ sung.

    `<StrictMode />` chỉ chạy trong chế độ phát triển (development mode). Các cảnh báo và kiểm tra này không xuất hiện trong môi trường sản phẩm (production), vì vậy nó không ảnh hưởng đến hiệu suất ứng dụng của người dùng cuối.

    Bạn có thể áp dụng `<StrictMode />` cho toàn bộ ứng dụng hoặc chỉ một phần của cây component. Điều này cho phép bạn áp dụng nó dần dần trong codebase lớn.

    import React from 'react';
    import ReactDOM from 'react-dom/client';
    import App from './App';
    
    const root = ReactDOM.createRoot(document.getElementById('root'));
    root.render(
      <React.StrictMode>
        <App />
      </React.StrictMode>
    );
    

    Khi được bật, Strict Mode kích hoạt các hành vi phát triển sau:

    1. Component sẽ re-render thêm một lần nữa trong chế độ phát triển: Giúp phát hiện các bug gây ra bởi việc rendering “không thuần khiết” (impure rendering), nơi hàm component có side effect (ảnh hưởng bên ngoài) hoặc thay đổi state/props ban đầu.
    2. Effects sẽ re-run thêm một lần nữa trong chế độ phát triển: Giúp phát hiện các bug do thiếu clean-up trong `useEffect`. Hàm clean-up (hàm trả về từ `useEffect`) sẽ chạy trước khi effect mới chạy, mô phỏng lại việc component bị mount/unmount nhanh.
    3. Callbacks của refs sẽ re-run thêm một lần trong chế độ phát triển: Giúp phát hiện các bug do thiếu clean-up trong callbacks của refs.
    4. Component sẽ được kiểm tra việc sử dụng các API đã deprecated (lỗi thời): Giúp bạn chuyển đổi sang các phương pháp hiện tại và tốt hơn.
    5. Phát hiện việc sử dụng các state management API legacy: Ví dụ như `findDOMNode`.

    Sử dụng Strict Mode là một thực hành tốt để tìm ra các vấn đề tiềm ẩn và đảm bảo component của bạn tuân thủ các best practices mới nhất của React.

    10. Cách xử lý lỗi một cách “duyên dáng” (Gracefully Handle Errors) trong React?

    Theo mặc định, nếu một lỗi xảy ra trong quá trình render (trong `render` method của class component hoặc trong thân hàm của functional component), React sẽ “gỡ bỏ” (unmount) toàn bộ cây component con bị lỗi khỏi giao diện và hiển thị một màn hình trống hoặc thông báo lỗi mặc định (tùy thuộc vào môi trường). Điều này có thể dẫn đến trải nghiệm người dùng tệ.

    Để ngăn chặn điều này và cung cấp một giao diện dự phòng (fallback UI) khi có lỗi, chúng ta sử dụng Error Boundaries.

    Error Boundary là một component React (hiện chỉ có thể được tạo bằng Class Component) mà:
    1. Bắt được lỗi JavaScript trong cây component con của nó (các lỗi xảy ra trong quá trình render, trong các lifecycle methods, và trong các constructor của toàn bộ cây bên dưới nó).
    2. Log lỗi đó.
    3. Hiển thị giao diện dự phòng thay vì làm hỏng toàn bộ cây component.

    Một Error Boundary không bắt được lỗi cho chính nó (ví dụ: lỗi trong render method của chính Error Boundary), cũng như các lỗi trong event handlers hoặc code bất đồng bộ (như `setTimeout`, promises).

    Bạn có thể tạo Error Boundary tùy chỉnh bằng cách sử dụng các lifecycle methods `static getDerivedStateFromError()` hoặc `componentDidCatch()`:

    import React from 'react';
    
    class ErrorBoundary extends React.Component {
      constructor(props) {
        super(props);
        this.state = { hasError: false };
      }
    
      // Lifecycle method này bắt lỗi trong quá trình render của các component con
      static getDerivedStateFromError(error) {
        // Cập nhật state để lần render tiếp theo hiển thị giao diện dự phòng
        return { hasError: true };
      }
    
      // Lifecycle method này bắt lỗi và cho phép log thông tin lỗi
      componentDidCatch(error, errorInfo) {
        // Bạn có thể gửi thông tin lỗi đến một dịch vụ báo cáo lỗi
        console.error("Uncaught error:", error, errorInfo);
      }
    
      render() {
        if (this.state.hasError) {
          // Bạn có thể hiển thị giao diện dự phòng tùy chỉnh
          // Hoặc nhận nó qua props
          return <h1>Đã xảy ra lỗi!</h1>;
          // return <FallbackComponent />; // Nếu bạn có component dự phòng
        }
    
        return this.props.children; // Render các component con bình thường
      }
    }
    
    // Cách sử dụng Error Boundary
    const ComponentThatMightError = () => {
      // Giả sử component này có thể gây lỗi trong quá trình render
      // throw new Error("Lỗi trong ComponentThatMightError!");
      return <div>Component hoạt động bình thường.</div>;
    };
    
    const App = () => (
      <ErrorBoundary>
        <ComponentThatMightError />
      </ErrorBoundary>
    );
    

    Đối với hầu hết các trường hợp, bạn có thể sử dụng các thư viện Error Boundary có sẵn như `react-error-boundary` để tiết kiệm thời gian viết mã boilerplate.

    import { ErrorBoundary } from "react-error-boundary";
    
    function Fallback({ error, resetErrorBoundary }) {
      // Component hiển thị khi có lỗi
      return (
        <div role="alert">
          <p>Đã xảy ra sự cố:</p>
          <pre>{error.message}</pre>
          <button onClick={resetErrorBoundary}>Thử lại</button>
        </div>
      );
    }
    
    const App = () => (
      <ErrorBoundary FallbackComponent={Fallback}>
        <MyComponent /> {/* Component có thể bị lỗi */}
      </ErrorBoundary>
    );
    

    11. Các quy tắc của React Hooks là gì?

    React Hooks là các hàm đặc biệt cho phép bạn “móc nối” (hook into) state và lifecycle features của React từ Functional Components. Để sử dụng Hooks một cách chính xác, bạn cần tuân thủ hai quy tắc cơ bản:

    1. Chỉ gọi Hooks từ các React Functions cấp cao nhất:
      • Hooks chỉ có thể được gọi từ bên trong React Functional Components hoặc từ bên trong Custom Hooks (các hook tự định nghĩa).
      • Bạn không được gọi Hooks bên trong các vòng lặp (`for`, `while`), các câu lệnh điều kiện (`if`, `else`), hoặc các hàm lồng nhau khác. Quy tắc này đảm bảo rằng Hooks luôn được gọi theo cùng một thứ tự trong mỗi lần component render. Điều này là cần thiết để React có thể kết nối state và side effects đúng với các lệnh gọi `useState`, `useEffect`, v.v. tương ứng.
    2. Chỉ gọi Hooks từ React Functions:
      • Nghĩa là bạn không được gọi Hooks từ các hàm JavaScript thông thường hoặc từ các Class Components. Hooks được thiết kế để hoạt động với Functional Components.

    Ngoài ra, theo quy ước (nhưng không phải là quy tắc bắt buộc về mặt kỹ thuật để Hook hoạt động), tên của Custom Hooks phải bắt đầu bằng từ `’use’` (ví dụ: `useMyCustomHook`). Quy ước này giúp React (và các công cụ Linting như ESLint) nhận diện rằng một hàm là Hook và có thể áp dụng các quy tắc của Hooks. Các Hook có sẵn của React cũng tuân thủ quy ước này (ví dụ: `useState`, `useEffect`, `useContext`).

    Vi phạm các quy tắc này sẽ gây ra các lỗi và cảnh báo (thường là lỗi “React Hook … is called in a way that is not supported”). Công cụ ESLint plugin eslint-plugin-react-hooks rất hữu ích trong việc tự động phát hiện các vi phạm này.

    12. Xử lý các Lifecycle Methods phổ biến trong React Functional Components như thế nào?

    Trong Class Components, chúng ta sử dụng các phương thức vòng đời như `componentDidMount`, `componentDidUpdate`, `componentWillUnmount` để thực hiện các hành động tại các giai đoạn khác nhau của component. Trong Functional Components, chúng ta sử dụng Hook `useEffect` để mô phỏng và xử lý các tác vụ tương tự.

    `useEffect` cho phép bạn thực hiện “side effects” (các tác vụ phụ như fetch data, subscribe event, thao tác trực tiếp DOM) trong functional components. Nó nhận hai đối số:

    1. Một hàm chứa code side effect.
    2. (Optional) Một mảng dependencies (các phụ thuộc). Effect sẽ chỉ chạy lại khi một trong các giá trị trong mảng này thay đổi.

    Mô phỏng `componentDidMount`: Effect chạy sau lần render đầu tiên (component mount) và chỉ một lần duy nhất.

    import React, { useEffect } from 'react';
    
    const MyComponent = () => {
      useEffect(() => {
        // Code ở đây chạy sau khi component được mount lần đầu tiên
        console.log('Component đã mount (componentDidMount tương đương)');
    
        // Thường dùng để:
        // - Fetch data lần đầu
        // - Thêm event listener
        // - Khởi tạo subscription
    
      }, []); // Mảng dependencies rỗng: effect chỉ chạy 1 lần sau mount
    };
    

    Mô phỏng `componentDidUpdate`: Effect chạy sau mỗi lần render mà các dependency thay đổi (và cả sau lần mount đầu tiên).

    import React, { useEffect, useState } from 'react';
    
    const MyComponent = () => {
      const [count, setCount] = useState(0);
      const [name, setName] = useState('');
    
      useEffect(() => {
        // Code ở đây chạy sau mỗi lần render nếu 'count' hoặc 'name' thay đổi
        console.log('Component đã update (componentDidUpdate tương đương)');
        console.log('Giá trị count hiện tại:', count);
        console.log('Giá trị name hiện tại:', name);
    
        // Thường dùng để:
        // - Fetch data khi một prop hoặc state cụ thể thay đổi
        // - Thực hiện logic dựa trên giá trị state/prop mới
    
      }, [count, name]); // Effect chạy lại khi 'count' hoặc 'name' thay đổi
    };
    

    Mô phỏng `componentWillUnmount` (Clean-up): Hàm clean-up được trả về từ effect sẽ chạy trước khi component bị unmount.

    import React, { useEffect } from 'react';
    
    const MyComponent = () => {
      useEffect(() => {
        console.log('Effect chạy (tương đương componentDidMount hoặc componentDidUpdate)');
    
        // Ví dụ: Thêm event listener
        const handleResize = () => console.log('Window resized');
        window.addEventListener('resize', handleResize);
    
        // Hàm được trả về là clean-up function
        return () => {
          // Code ở đây chạy trước khi component bị unmount
          // Hoặc trước khi effect chạy lại (nếu dependencies thay đổi)
          console.log('Clean-up chạy (tương đương componentWillUnmount hoặc trước khi update)');
    
          // Thường dùng để:
          // - Xóa event listener
          // - Hủy subscription
          // - Hủy timer/interval
          // - Hủy yêu cầu mạng (cancel network requests)
    
          window.removeEventListener('resize', handleResize);
        };
      }, []); // Mảng dependencies rỗng, clean-up chỉ chạy khi unmount
    };
    

    Bằng cách kết hợp `useEffect` với mảng dependencies và hàm clean-up, Functional Components có thể xử lý hầu hết các trường hợp sử dụng của các lifecycle methods trong Class Components.

    13. Refs trong React là gì?

    Refs (viết tắt của References) trong React là một cách để truy cập trực tiếp vào các phần tử DOM hoặc các instance của component Class, cũng như để lưu trữ các giá trị có thể tồn tại giữa các lần render mà không gây ra re-render khi chúng thay đổi.

    Trong Functional Components, bạn sử dụng Hook `useRef()` để tạo ref. `useRef()` trả về một đối tượng có thuộc tính `.current` ban đầu được gán giá trị khởi tạo. Thuộc tính `.current` có thể được thay đổi và đọc ở bất kỳ đâu trong component.

    Công dụng phổ biến của Refs:

    1. Truy cập phần tử DOM: Đây là trường hợp sử dụng phổ biến nhất. Bạn có thể đính kèm ref vào một phần tử DOM (ví dụ: ``) để sau đó truy cập trực tiếp đến phần tử DOM đó thông qua `inputRef.current`. Điều này hữu ích cho các tác vụ như tập trung vào một input, đo kích thước phần tử, kích hoạt animation, v.v.
    2. Lưu trữ giá trị có thể tồn tại giữa các lần render: Refs có thể lưu trữ bất kỳ giá trị nào (không chỉ phần tử DOM). Không giống như state, việc thay đổi giá trị của `ref.current` không kích hoạt re-render component. Điều này rất hữu ích để lưu trữ các giá trị cần được giữ lại giữa các lần render nhưng không ảnh hưởng trực tiếp đến giao diện hiển thị (ví dụ: timer IDs, giá trị trước đó của state/props, flag `isMounted`).
    3. Truy cập instance của Class Component: Bạn có thể đính kèm ref vào một Class Component để gọi các phương thức công khai của instance đó (ít phổ biến hơn với sự ưu tiên của Functional Components và Hooks).

    Ví dụ truy cập DOM element:

    import React, { useRef } from 'react';
    
    const FocusInputButton = () => {
      const inputRef = useRef(null); // Khởi tạo ref với giá trị null
    
      const handleClick = () => {
        // Truy cập phần tử DOM thông qua inputRef.current
        if (inputRef.current) {
          inputRef.current.focus(); // Gọi phương thức focus() của DOM input element
        }
      };
    
      return (
        <div>
          <input ref={inputRef} type="text" /> {/* Đính kèm ref vào input */}
          <button onClick={handleClick}>Focus Input</button>
        </div>
      );
    };
    

    Ví dụ lưu trữ giá trị:

    import React, { useRef, useEffect, useState } from 'react';
    
    const Timer = () => {
      const intervalRef = useRef(null); // Lưu trữ ID của interval
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        // Bắt đầu interval và lưu ID vào ref
        intervalRef.current = setInterval(() => {
          setCount(prevCount => prevCount + 1);
        }, 1000);
    
        // Clean-up function để xóa interval khi component unmount
        return () => {
          clearInterval(intervalRef.current);
        };
      }, []); // Effect chạy 1 lần khi mount
    
      const handleStop = () => {
        // Truy cập ID interval từ ref để xóa
        clearInterval(intervalRef.current);
      };
    
      return (
        <div>
          <p>Đếm: {count}</p>
          <button onClick={handleStop}>Dừng đếm</button>
        </div>
      );
    };
    

    Refs là một công cụ mạnh mẽ, nhưng nên được sử dụng một cách cẩn thận. Hầu hết các tương tác và cập nhật giao diện nên được xử lý thông qua state và props, thay vì thao tác trực tiếp với DOM thông qua refs.

    14. Prop Drilling trong React là gì? Làm thế nào để tránh nó?

    Prop Drilling (Khoan Props) là một vấn đề phổ biến trong các ứng dụng React khi dữ liệu hoặc các hàm callback cần được truyền từ một component cấp cao xuống một component cấp thấp, sâu trong cây component hierarchy. Điều này xảy ra khi bạn phải truyền props qua nhiều component trung gian mà bản thân các component trung gian đó không hề sử dụng các props đó, chúng chỉ đơn giản là “chuyển tiếp” chúng xuống component con của mình.

    Nhược điểm của Prop Drilling:

    1. Tăng độ phức tạp: Component trung gian phải nhận và truyền các props mà nó không cần, làm cho code khó đọc và khó hiểu hơn.
    2. Giảm khả năng tái sử dụng: Các component trung gian trở nên phụ thuộc vào các props mà lẽ ra chúng không cần biết, làm giảm khả năng tái sử dụng chúng ở những nơi khác.
    3. Khó bảo trì: Khi cấu trúc dữ liệu thay đổi hoặc component cấp thấp cần thêm một prop mới, bạn có thể phải sửa đổi nhiều component trung gian trong chuỗi “khoan” props.
    4. Ảnh hưởng hiệu suất (tiềm ẩn): Mặc dù React khá hiệu quả, việc truyền quá nhiều props có thể ảnh hưởng đến hiệu suất trong các trường hợp render phức tạp, đặc biệt nếu không sử dụng memoization đúng cách.

    Làm thế nào để tránh Prop Drilling?

    Có hai kỹ thuật chính để tránh hoặc giảm thiểu Prop Drilling:

    1. Context API: React Context cung cấp một cách để chia sẻ dữ liệu (như state, functions, settings) xuyên suốt cây component mà không cần truyền props thủ công ở mỗi cấp độ. Bạn tạo một Context, một component Provider ở cấp cao để cung cấp dữ liệu, và các component con ở bất kỳ cấp độ nào có thể sử dụng Hook `useContext` (hoặc Consumer trong Class Components) để truy cập dữ liệu đó. Context rất phù hợp cho dữ liệu mang tính “global” hoặc chia sẻ cho nhiều component không liền kề như thông tin người dùng, theme, cài đặt ngôn ngữ.
    2. State Management Libraries (Các thư viện quản lý trạng thái): Đối với các ứng dụng lớn và phức tạp với state được chia sẻ rộng rãi và logic phức tạp, các thư viện quản lý trạng thái chuyên dụng như Redux, Zustand, Jotai, Recoil, hoặc MobX là giải pháp mạnh mẽ. Các thư viện này cung cấp một “store” (kho lưu trữ) tập trung cho state của ứng dụng, và các component có thể kết nối trực tiếp với store để đọc hoặc ghi state mà không cần thông qua props.

    Việc lựa chọn giữa Context API và thư viện quản lý trạng thái phụ thuộc vào quy mô và độ phức tạp của ứng dụng.

    15. Mô tả một số kỹ thuật tối ưu hóa hiệu suất trong React

    Tối ưu hóa hiệu suất là một khía cạnh quan trọng khi phát triển ứng dụng React, đặc biệt đối với các ứng dụng lớn. Dưới đây là một số kỹ thuật phổ biến:

    a) Sử dụng Memoization

    `useMemo``useCallback` (cho hàm) là các Hooks giúp ghi nhớ (cache) kết quả của các tính toán phức tạp hoặc các hàm. Nếu các dependencies (phụ thuộc) của `useMemo` hoặc `useCallback` không thay đổi giữa các lần render, React sẽ trả về giá trị đã được ghi nhớ thay vì chạy lại hàm hoặc tính toán. Điều này hữu ích để tránh các tính toán lại tốn kém hoặc ngăn component con re-render không cần thiết nếu chúng nhận props là hàm hoặc giá trị được tạo ra từ tính toán phức tạp.

    `React.memo` là một Higher-Order Component (HOC) giúp ghi nhớ toàn bộ một Functional Component. Nếu props của component được bọc bởi `React.memo` không thay đổi, React sẽ bỏ qua việc render lại component đó.

    import React, { useMemo } from 'react';
    
    const ComplexCalculationComponent = ({ data }) => {
      // Chỉ chạy lại hàm calculate khi 'data' thay đổi
      const calculatedResult = useMemo(() => {
        console.log('Thực hiện tính toán phức tạp...');
        // Giả sử calculate là một hàm tốn nhiều tài nguyên CPU
        return calculate(data);
      }, [data]); // Dependency array
    
      return <div>Kết quả tính toán: {calculatedResult}</div>;
    };
    
    // Sử dụng React.memo để ngăn MyMemoizedComponent re-render nếu props không đổi
    const MyMemoizedComponent = React.memo(({ value }) => {
      console.log('Rendering MyMemoizedComponent');
      return <div>Giá trị: {value}</div>;
    });
    

    b) Lazy Loading (Tải lười) và Code Splitting

    Kỹ thuật này giúp giảm thời gian tải ban đầu của ứng dụng bằng cách chỉ tải code cho các component hoặc route mà người dùng đang xem. Các phần code khác sẽ được tải theo yêu cầu (on-demand) khi người dùng điều hướng đến chúng. React hỗ trợ `React.lazy` kết hợp với `React.Suspense` để thực hiện lazy loading cho component.

    import React, { lazy, Suspense } from 'react';
    
    // Lazy load component MyHeavyComponent
    const MyHeavyComponent = lazy(() => import('./MyHeavyComponent'));
    
    const App = () => (
      <div>
        <h1>Ứng dụng của tôi</h1>
        {/* Hiển thị giao diện dự phòng trong khi MyHeavyComponent đang được tải */}
        <Suspense fallback={<div>Đang tải...</div>}>
          <MyHeavyComponent />
        </Suspense>
      </div>
    );
    

    c) Throttling và Debouncing

    Đây là các kỹ thuật JavaScript chung nhưng rất hữu ích trong React để giới hạn số lần một hàm được gọi. Thường được sử dụng với các sự kiện xảy ra liên tục như `scroll`, `resize`, hoặc nhập liệu vào input.

    • Debouncing: Chỉ gọi hàm sau một khoảng thời gian trễ nhất định kể từ lần cuối cùng sự kiện xảy ra. Hữu ích cho input search (chỉ gọi API sau khi người dùng ngừng gõ một lúc).
    • Throttling: Chỉ cho phép hàm được gọi tối đa một lần trong một khoảng thời gian nhất định, bất kể sự kiện xảy ra bao nhiêu lần. Hữu ích cho xử lý sự kiện cuộn trang (scroll).

    Bạn có thể sử dụng các thư viện như Lodash hoặc viết hàm riêng để thực hiện debouncing/throttling.

    d) Virtualization (List Virtualization)

    Đối với các danh sách dài (hàng trăm, hàng nghìn item), rendering toàn bộ danh sách cùng lúc sẽ gây hiệu suất kém. Virtualization (hoặc windowing) chỉ render các item hiện đang hiển thị trong viewport của người dùng, và tái sử dụng các phần tử DOM khi người dùng cuộn. Các thư viện như react-windowreact-virtualized giúp triển khai kỹ thuật này.

    16. Portals trong React là gì?

    Portals trong React cung cấp một cách để render children (các phần tử con) vào một nút DOM tồn tại bên ngoài hệ thống phân cấp DOM của component cha. Thông thường, children của một component React được render vào bên trong phần tử DOM của component cha.

    Tuy nhiên, có những trường hợp bạn cần “teleport” một phần UI ra ngoài vị trí thông thường trong cây DOM, ví dụ:

    • Modals (hộp thoại Pop-up): Thường cần hiển thị ở trên cùng của trang (ví dụ: trực tiếp dưới thẻ ``) để tránh bị ảnh hưởng bởi các style CSS của component cha (như `z-index`, `overflow: hidden`, `position`).
    • Tooltips, Popovers: Cũng có thể cần render ở vị trí độc lập để đảm bảo hiển thị đúng.
    • Widgets của bên thứ ba: Đôi khi cần nhúng nội dung vào một phần tử DOM đã được định nghĩa sẵn bên ngoài root của ứng dụng React.

    Bạn tạo một Portal bằng hàm `ReactDOM.createPortal()`. Hàm này nhận hai đối số:

    1. Phần tử React hoặc các phần tử mà bạn muốn render (`children`).
    2. Nút DOM đích mà bạn muốn render các phần tử đó vào.

    Thông thường, bạn sẽ tạo một nút DOM đặc biệt trong tệp `index.html` của mình (ví dụ: `

    `) và sử dụng nó làm đích cho portal.

    Ví dụ:

    import React from 'react';
    import ReactDOM from 'react-dom';
    
    const modalRoot = document.getElementById('modal-root'); // Giả sử có <div id="modal-root"> trong index.html
    
    const Modal = ({ children, isOpen }) => {
      if (!isOpen) return null; // Không hiển thị modal nếu isOpen là false
    
      // Sử dụng createPortal để render nội dung modal vào nút modalRoot
      return ReactDOM.createPortal(
        <div style={{
          position: 'fixed',
          top: 0,
          left: 0,
          right: 0,
          bottom: 0,
          backgroundColor: 'rgba(0,0,0,0.5)',
          display: 'flex',
          justifyContent: 'center',
          alignItems: 'center',
          zIndex: 1000 // Đảm bảo modal nằm trên cùng
        }}>
          <div style={{
            backgroundColor: 'white',
            padding: '20px',
            borderRadius: '5px'
          }}>
            {children}
          </div>
        </div>,
        modalRoot // Render vào nút DOM có ID 'modal-root'
      );
    };
    
    // Component sử dụng Modal
    const App = () => {
      const [isModalOpen, setIsModalOpen] = React.useState(false);
    
      return (
        <div>
          <h1>Ứng dụng chính</h1>
          <button onClick={() => setIsModalOpen(true)}>Mở Modal</button>
    
          <Modal isOpen={isModalOpen}>
            <h2>Nội dung Modal</h2>
            <p>Đây là nội dung của hộp thoại modal.</p>
            <button onClick={() => setIsModalOpen(false)}>Đóng Modal</button>
          </Modal>
    
          <div style={{ overflow: 'hidden', height: '100px', border: '1px solid red' }}>
            <p>Một số nội dung khác trong ứng dụng. Modal sẽ hiển thị bên ngoài vùng này.</p>
          </div>
        </div>
      );
    };
    

    Mặc dù nội dung được render vào một nút DOM khác, Portal vẫn duy trì các tính năng của React như Context và event propagation (sự kiện vẫn nổi bọt lên cây component React ban đầu).

    17. React Fiber là gì?

    React Fiber là một sự thay đổi lớn trong kiến trúc cốt lõi của React (một sự tái cấu trúc lại bộ “reconciler” – bộ điều phối). Nó được giới thiệu từ React 16 và là nền tảng cho các tính năng mới như Suspense và Concurrent Mode. Fiber không phải là một khái niệm mà bạn tương tác trực tiếp khi viết component, mà là cơ chế nội bộ giúp React hoạt động hiệu quả hơn.

    Mục tiêu chính của React Fiber là cho phép React xử lý quá trình rendering và cập nhật một cách bất đồng bộ (asynchronous). Trước Fiber, React sử dụng một bộ điều phối “stack-based” đồng bộ, có nghĩa là khi React bắt đầu xử lý một cập nhật, nó phải hoàn thành toàn bộ quá trình cho cây component đó mà không bị gián đoạn.

    Với kiến trúc Fiber, quá trình reconciliation (đối soát) được chia thành các “đơn vị công việc” (units of work) nhỏ gọi là Fibers. Mỗi Fiber đại diện cho một đơn vị công việc cần thực hiện, thường tương ứng với một instance của component hoặc một phần tử DOM. Quá trình reconciliation trở thành việc đi qua cây Fiber, xác định những thay đổi cần thiết.

    Nhờ cơ chế bất đồng bộ, React Fiber có thể:

    • Tạm dừng (pause), tiếp tục (resume), và hủy bỏ (abort) công việc render khi có các cập nhật mới hoặc các tác vụ ưu tiên cao hơn xuất hiện.
    • Chia công việc render thành các đoạn nhỏ (chunks) và phân bổ chúng qua nhiều frame trình duyệt.
    • Ưu tiên các tác vụ dựa trên mức độ quan trọng của chúng (ví dụ: các cập nhật cho animation hoặc input người dùng có thể được ưu tiên hơn việc tải dữ liệu nền).

    Điều này giúp React duy trì ứng dụng phản ứng nhanh hơn, tránh hiện tượng “chặn luồng chính” (blocking the main thread) của trình duyệt, đặc biệt hữu ích cho các animation mượt mà và trải nghiệm người dùng tốt hơn trên các thiết bị hiệu suất thấp.

    Tóm lại, React Fiber là công cụ nội bộ mạnh mẽ giúp React quản lý và xử lý cây component, các cập nhật một cách hiệu quả, bất đồng bộ và ưu tiên, là nền tảng cho hiệu suất và các tính năng hiện đại của React.

    Bạn có thắc mắc “Reconciliation” là gì không?

    Trong React, reconciliation (đối soát) là quá trình mà React sử dụng để cập nhật UI của ứng dụng sao cho khớp với trạng thái mới nhất. Khi state hoặc props của một component thay đổi, React sẽ tạo ra một cây Virtual DOM mới và so sánh nó với cây Virtual DOM cũ (quá trình “diffing”). Dựa trên sự khác biệt tìm thấy, React sẽ xác định những thay đổi tối thiểu cần thực hiện lên Real DOM để cập nhật giao diện. Fiber là thuật toán reconciliation mới (từ React 16) thực hiện quá trình so sánh và cập nhật này một cách hiệu quả và bất đồng bộ.

    Đó là tất cả! 🎉

    Chúc bạn ôn tập tốt và thành công trong buổi phỏng vấn React sắp tới vào năm 2025!

  • Chỉ mục