Top 30 câu hỏi phỏng vấn React: Hướng dẫn toàn diện để thành công

30 câu hỏi phỏng vấn React được hỏi nhiều nhất năm 2026. Câu trả lời chi tiết, ví dụ mã và lời khuyên để có được vị trí React developer.

Minh họa câu hỏi phỏng vấn React với các component và hook được kết nối với nhau

Các buổi phỏng vấn kỹ thuật React đánh giá mức độ hiểu biết về khái niệm nền tảng, pattern nâng cao và các thực hành tốt nhất. Hướng dẫn này tổng hợp 30 câu hỏi xuất hiện thường xuyên nhất kèm câu trả lời chi tiết và ví dụ mã, giúp việc chuẩn bị trở nên hiệu quả.

Lời khuyên chuẩn bị

Các câu hỏi được sắp xếp theo mức độ khó. Việc nắm vững các khái niệm nền tảng trước khi tiến đến các chủ đề nâng cao giúp quá trình ôn luyện có cấu trúc hơn.

Nền tảng React

1. Virtual DOM là gì và vì sao React sử dụng nó?

Virtual DOM là một biểu diễn nhẹ bằng JavaScript của DOM thật. React dùng lớp trừu tượng này để tối ưu hóa việc cập nhật giao diện.

Quy trình gồm ba bước: trước tiên React tạo một bản sao ảo của DOM, sau đó so sánh bản sao này với phiên bản trước khi có thay đổi (thuật toán diffing) và cuối cùng chỉ áp dụng những thay đổi cần thiết lên DOM thật (reconciliation).

jsx
// Simplified example of the concept
// When state changes, React doesn't recreate the entire DOM
function Counter() {
  const [count, setCount] = useState(0)

  // Only the span containing count will be updated in the real DOM
  // The rest of the component is untouched
  return (
    <div>
      <h1>Counter</h1>
      <span>{count}</span>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  )
}

Cách tiếp cận này tránh được các thao tác tốn kém trên DOM và cho phép cập nhật mượt mà ngay cả với giao diện phức tạp.

2. Sự khác nhau giữa component hàm và component lớp là gì?

Component hàm là các hàm JavaScript nhận props và trả về JSX. Từ React 16.8, hooks cho phép dùng state và vòng đời ngay trong component hàm.

jsx
// Functional component (recommended)
// More concise, easier to test, supports hooks
function Welcome({ name }) {
  const [visits, setVisits] = useState(0)

  useEffect(() => {
    setVisits(v => v + 1)
  }, [])

  return <h1>Hello {name}, visit #{visits}</h1>
}

// Class component (legacy)
// More verbose, requires this binding
class WelcomeClass extends React.Component {
  state = { visits: 0 }

  componentDidMount() {
    this.setState(prev => ({ visits: prev.visits + 1 }))
  }

  render() {
    return <h1>Hello {this.props.name}, visit #{this.state.visits}</h1>
  }
}

Component hàm hiện là chuẩn mực. Component lớp vẫn được hỗ trợ nhưng không còn được khuyến nghị cho các dự án mới.

3. JSX hoạt động như thế nào?

JSX là phần mở rộng cú pháp của JavaScript cho phép viết markup trực tiếp trong mã. Đây không phải HTML mà là JavaScript được ngụy trang.

jsx
// What we write (JSX)
const element = (
  <div className="container">
    <h1>Title</h1>
    <p>Paragraph</p>
  </div>
)

// What Babel compiles (pure JavaScript)
const element = React.createElement(
  'div',
  { className: 'container' },
  React.createElement('h1', null, 'Title'),
  React.createElement('p', null, 'Paragraph')
)

Các khác biệt so với HTML gồm: className thay cho class, htmlFor thay cho for, thuộc tính theo dạng camelCase (onClick, tabIndex) và yêu cầu đóng các thẻ tự đóng.

4. State và props khác nhau ở đâu?

Props là dữ liệu được truyền từ component cha sang component con, chỉ đọc. State là trạng thái nội bộ của component và có thể thay đổi thông qua setter.

UserCard.jsxjsx
// name and role are props (immutable)
function UserCard({ name, role }) {
  // isExpanded is state (mutable)
  const [isExpanded, setIsExpanded] = useState(false)

  return (
    <div className="card">
      <h2>{name}</h2>
      <p>{role}</p>

      {/* Modifying state triggers a re-render */}
      <button onClick={() => setIsExpanded(!isExpanded)}>
        {isExpanded ? 'Collapse' : 'Details'}
      </button>

      {isExpanded && <UserDetails name={name} />}
    </div>
  )
}

// Usage
<UserCard name="Alice" role="Developer" />

Nguyên tắc cơ bản: props chảy xuống (cha → con), còn state là cục bộ cho từng component.

5. Vì sao key lại quan trọng trong danh sách?

Key giúp React xác định phần tử nào đã thay đổi, được thêm hay xóa khỏi danh sách. Khi thiếu key duy nhất và ổn định, React có thể có những hành vi không lường trước.

jsx
// ❌ Bad practice: index as key
// Problem: if order changes, React loses tracking
{items.map((item, index) => (
  <ListItem key={index} item={item} />
))}

// ✅ Good practice: unique and stable identifier
{items.map(item => (
  <ListItem key={item.id} item={item} />
))}

// Concrete example of the problem with indices
function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React' },
    { id: 2, text: 'Create a project' }
  ])

  // When deleting the first element with key={index}
  // React will think element 0's content changed
  // instead of understanding an element was removed

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  )
}

React Hooks

6. Giải thích useState và những cạm bẫy thường gặp

useState quản lý state cục bộ trong một component hàm. Setter có thể nhận một giá trị hoặc một hàm cập nhật.

jsx
// Declaration with initial value
const [count, setCount] = useState(0)

// ❌ Pitfall: multiple updates in the same cycle
function increment() {
  setCount(count + 1) // count = 0, sets 1
  setCount(count + 1) // count = 0 still, sets 1
  setCount(count + 1) // count = 0 still, sets 1
  // Final result: 1 (not 3)
}

// ✅ Solution: use the update function
function incrementCorrect() {
  setCount(prev => prev + 1) // 0 → 1
  setCount(prev => prev + 1) // 1 → 2
  setCount(prev => prev + 1) // 2 → 3
  // Final result: 3
}

// ❌ Pitfall: object mutation
const [user, setUser] = useState({ name: 'Alice', age: 25 })
user.age = 26 // Direct mutation, no re-render

// ✅ Solution: create a new object
setUser({ ...user, age: 26 })
// or
setUser(prev => ({ ...prev, age: 26 }))

7. useEffect hoạt động thế nào với mảng dependency?

useEffect thực thi các hiệu ứng phụ sau khi render. Mảng dependency quyết định thời điểm hiệu ứng được chạy.

jsx
// Executed on every render (rare, usually avoid)
useEffect(() => {
  console.log('Render completed')
})

// Executed only on mount (equivalent to componentDidMount)
useEffect(() => {
  console.log('Component mounted')

  // Cleanup on unmount (equivalent to componentWillUnmount)
  return () => {
    console.log('Component unmounted')
  }
}, [])

// Executed when userId changes
useEffect(() => {
  async function fetchUser() {
    const response = await fetch(`/api/users/${userId}`)
    const data = await response.json()
    setUser(data)
  }

  fetchUser()
}, [userId])

// ❌ Missing dependency - subtle bug
useEffect(() => {
  const timer = setInterval(() => {
    setCount(count + 1) // count is "captured" at its initial value
  }, 1000)
  return () => clearInterval(timer)
}, []) // count is missing from dependencies

// ✅ Fix with update function
useEffect(() => {
  const timer = setInterval(() => {
    setCount(prev => prev + 1) // No need for count in deps
  }, 1000)
  return () => clearInterval(timer)
}, [])
Quy tắc ESLint

Nên luôn bật eslint-plugin-react-hooks để phát hiện các dependency bị thiếu. Quy tắc này ngăn chặn nhiều lỗi khó chẩn đoán.

8. Khi nào nên dùng useMemo và useCallback?

Hai hook này cho phép ghi nhớ (memoization) để tránh tính toán lại hoặc tạo lại không cần thiết. Tuy nhiên, không nên lạm dụng.

jsx
// useMemo: memoizes a computed value
function ProductList({ products, filter }) {
  // Recalculated only if products or filter change
  const filteredProducts = useMemo(() => {
    console.log('Filtering...')
    return products.filter(p => p.category === filter)
  }, [products, filter])

  return <ul>{filteredProducts.map(p => <li key={p.id}>{p.name}</li>)}</ul>
}

// useCallback: memoizes a function
function ParentComponent() {
  const [count, setCount] = useState(0)

  // Without useCallback, handleClick is recreated on every render
  // Causing unnecessary re-renders of ExpensiveChild
  const handleClick = useCallback((id) => {
    console.log('Clicked:', id)
  }, []) // Empty deps = stable function

  return (
    <>
      <span>{count}</span>
      <button onClick={() => setCount(c => c + 1)}>+</button>
      {/* React.memo on ExpensiveChild for this to be effective */}
      <ExpensiveChild onClick={handleClick} />
    </>
  )
}

// ❌ Over-optimization: not needed here
const SimpleComponent = () => {
  // This calculation is trivial, useMemo adds overhead
  const doubled = useMemo(() => 2 * 2, [])
  return <span>{doubled}</span>
}

Chỉ nên dùng các hook này khi có vấn đề hiệu năng đã được xác định, hoặc để ổn định tham chiếu được truyền vào các component đã được memo.

9. useRef hoạt động ra sao và dùng trong những trường hợp nào?

useRef tạo ra một tham chiếu có thể thay đổi, tồn tại qua các lần render mà không kích hoạt render lại khi giá trị thay đổi.

jsx
// Case 1: Access a DOM element
function TextInput() {
  const inputRef = useRef(null)

  const focusInput = () => {
    inputRef.current.focus()
  }

  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus</button>
    </>
  )
}

// Case 2: Store a mutable value without re-render
function Timer() {
  const [seconds, setSeconds] = useState(0)
  const intervalRef = useRef(null)

  const start = () => {
    // Store the interval ID to be able to stop it
    intervalRef.current = setInterval(() => {
      setSeconds(s => s + 1)
    }, 1000)
  }

  const stop = () => {
    clearInterval(intervalRef.current)
  }

  return (
    <div>
      <span>{seconds}s</span>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  )
}

// Case 3: Keep the previous value
function usePrevious(value) {
  const ref = useRef()

  useEffect(() => {
    ref.current = value
  }, [value])

  return ref.current
}

10. Giải thích useContext và khi nào nên dùng

useContext truy cập vào một React context mà không phải truyền props qua nhiều cấp. Phù hợp cho dữ liệu toàn cục như theme hoặc người dùng đang đăng nhập.

1. Create the contextjsx
const ThemeContext = createContext({
  theme: 'light',
  toggleTheme: () => {}
})

// 2. Create the provider
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')

  const toggleTheme = useCallback(() => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light')
  }, [])

  // Memoize the value to avoid unnecessary re-renders
  const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme])

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  )
}

// 3. Use the context
function ThemedButton() {
  const { theme, toggleTheme } = useContext(ThemeContext)

  return (
    <button
      onClick={toggleTheme}
      className={theme === 'dark' ? 'bg-gray-800 text-white' : 'bg-white text-gray-800'}
    >
      {theme === 'dark' ? 'Light' : 'Dark'} mode
    </button>
  )
}

// 4. Wrap the application
function App() {
  return (
    <ThemeProvider>
      <Header />
      <Main />
      <Footer />
    </ThemeProvider>
  )
}

Sẵn sàng chinh phục phỏng vấn React / Next.js?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

Các pattern nâng cao

11. Higher-Order Component (HOC) là gì?

HOC là một hàm nhận vào một component và trả về một component mới đã được mở rộng. Ít dùng hơn kể từ khi có hooks nhưng vẫn xuất hiện trong một số thư viện.

jsx
// HOC that adds logging
function withLogging(WrappedComponent) {
  return function WithLogging(props) {
    useEffect(() => {
      console.log(`${WrappedComponent.name} mounted with props:`, props)

      return () => {
        console.log(`${WrappedComponent.name} unmounted`)
      }
    }, [])

    return <WrappedComponent {...props} />
  }
}

// HOC that handles authentication
function withAuth(WrappedComponent) {
  return function WithAuth(props) {
    const { user, isLoading } = useAuth()

    if (isLoading) return <LoadingSpinner />
    if (!user) return <Navigate to="/login" />

    return <WrappedComponent {...props} user={user} />
  }
}

// Usage
const ProtectedDashboard = withAuth(Dashboard)
const LoggedButton = withLogging(Button)

12. Giải thích pattern Render Props

Pattern Render Props chia sẻ logic giữa các component thông qua một prop là một hàm.

jsx
// Component with render prop
function MouseTracker({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 })

  useEffect(() => {
    const handleMove = (e) => {
      setPosition({ x: e.clientX, y: e.clientY })
    }

    window.addEventListener('mousemove', handleMove)
    return () => window.removeEventListener('mousemove', handleMove)
  }, [])

  // Call the render function with data
  return render(position)
}

// Usage
function App() {
  return (
    <MouseTracker
      render={({ x, y }) => (
        <div>
          Position: {x}, {y}
        </div>
      )}
    />
  )
}

// Modern version with custom hook (preferred)
function useMousePosition() {
  const [position, setPosition] = useState({ x: 0, y: 0 })

  useEffect(() => {
    const handleMove = (e) => {
      setPosition({ x: e.clientX, y: e.clientY })
    }

    window.addEventListener('mousemove', handleMove)
    return () => window.removeEventListener('mousemove', handleMove)
  }, [])

  return position
}

function App() {
  const { x, y } = useMousePosition()
  return <div>Position: {x}, {y}</div>
}

13. Cách tạo một custom hook?

Custom hook dùng để tách và tái sử dụng logic có state giữa các component.

useLocalStorage.jsjsx
function useLocalStorage(key, initialValue) {
  // Initialize with localStorage value or default
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key)
      return item ? JSON.parse(item) : initialValue
    } catch (error) {
      console.error(error)
      return initialValue
    }
  })

  // Setter wrapper that syncs with localStorage
  const setValue = useCallback((value) => {
    try {
      // Support update functions
      const valueToStore = value instanceof Function ? value(storedValue) : value
      setStoredValue(valueToStore)
      window.localStorage.setItem(key, JSON.stringify(valueToStore))
    } catch (error) {
      console.error(error)
    }
  }, [key, storedValue])

  return [storedValue, setValue]
}

// Usage
function Settings() {
  const [theme, setTheme] = useLocalStorage('theme', 'light')
  const [fontSize, setFontSize] = useLocalStorage('fontSize', 16)

  return (
    <div>
      <select value={theme} onChange={e => setTheme(e.target.value)}>
        <option value="light">Light</option>
        <option value="dark">Dark</option>
      </select>

      <input
        type="range"
        min="12"
        max="24"
        value={fontSize}
        onChange={e => setFontSize(Number(e.target.value))}
      />
    </div>
  )
}

14. Compound Components Pattern là gì?

Pattern này tạo ra các component phối hợp ngầm với nhau, giống như các thẻ <select><option>.

jsx
// Shared context between components
const TabsContext = createContext()

function Tabs({ children, defaultValue }) {
  const [activeTab, setActiveTab] = useState(defaultValue)

  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  )
}

function TabList({ children }) {
  return <div className="tab-list" role="tablist">{children}</div>
}

function Tab({ value, children }) {
  const { activeTab, setActiveTab } = useContext(TabsContext)
  const isActive = activeTab === value

  return (
    <button
      role="tab"
      aria-selected={isActive}
      className={`tab ${isActive ? 'active' : ''}`}
      onClick={() => setActiveTab(value)}
    >
      {children}
    </button>
  )
}

function TabPanel({ value, children }) {
  const { activeTab } = useContext(TabsContext)

  if (activeTab !== value) return null

  return (
    <div role="tabpanel" className="tab-panel">
      {children}
    </div>
  )
}

// Attach sub-components
Tabs.List = TabList
Tabs.Tab = Tab
Tabs.Panel = TabPanel

// Intuitive usage
function App() {
  return (
    <Tabs defaultValue="profile">
      <Tabs.List>
        <Tabs.Tab value="profile">Profile</Tabs.Tab>
        <Tabs.Tab value="settings">Settings</Tabs.Tab>
        <Tabs.Tab value="billing">Billing</Tabs.Tab>
      </Tabs.List>

      <Tabs.Panel value="profile">Profile content...</Tabs.Panel>
      <Tabs.Panel value="settings">Settings content...</Tabs.Panel>
      <Tabs.Panel value="billing">Billing content...</Tabs.Panel>
    </Tabs>
  )
}

15. Cách triển khai pattern Controlled và Uncontrolled?

Component controlled có state được React quản lý, còn component uncontrolled sử dụng trực tiếp DOM.

jsx
// CONTROLLED component
// State is in React, component reflects that state
function ControlledInput() {
  const [value, setValue] = useState('')

  const handleSubmit = (e) => {
    e.preventDefault()
    console.log('Submitted value:', value)
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
      <button type="submit">Submit</button>
    </form>
  )
}

// UNCONTROLLED component
// State is in the DOM, we read it when needed
function UncontrolledInput() {
  const inputRef = useRef(null)

  const handleSubmit = (e) => {
    e.preventDefault()
    console.log('Submitted value:', inputRef.current.value)
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        ref={inputRef}
        defaultValue=""
      />
      <button type="submit">Submit</button>
    </form>
  )
}

// Component that supports BOTH modes
function FlexibleInput({ value, defaultValue, onChange }) {
  const isControlled = value !== undefined
  const [internalValue, setInternalValue] = useState(defaultValue || '')

  const currentValue = isControlled ? value : internalValue

  const handleChange = (e) => {
    if (!isControlled) {
      setInternalValue(e.target.value)
    }
    onChange?.(e.target.value)
  }

  return (
    <input
      type="text"
      value={currentValue}
      onChange={handleChange}
    />
  )
}

Hiệu năng và tối ưu hóa

16. React.memo hoạt động như thế nào?

React.memo là một HOC giúp ghi nhớ một component để tránh render lại khi props không thay đổi.

jsx
// Memoized component
const ExpensiveList = React.memo(function ExpensiveList({ items, onItemClick }) {
  console.log('ExpensiveList rendered')

  return (
    <ul>
      {items.map(item => (
        <li key={item.id} onClick={() => onItemClick(item.id)}>
          {item.name}
        </li>
      ))}
    </ul>
  )
})

// Parent using the memoized component
function Parent() {
  const [count, setCount] = useState(0)
  const [items] = useState([
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' }
  ])

  // ❌ This function is recreated on every render
  // So ExpensiveList re-renders despite memo
  const handleClick = (id) => console.log(id)

  // ✅ Stable function with useCallback
  const handleClickStable = useCallback((id) => {
    console.log(id)
  }, [])

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      <ExpensiveList items={items} onItemClick={handleClickStable} />
    </div>
  )
}

// Custom comparison
const MemoWithCustomCompare = React.memo(
  function Component({ user, onClick }) {
    return <div onClick={onClick}>{user.name}</div>
  },
  (prevProps, nextProps) => {
    // Return true if props are equal (skip re-render)
    return prevProps.user.id === nextProps.user.id
  }
)

17. Code splitting và React.lazy là gì?

Code splitting chia bundle thành nhiều phần được tải theo nhu cầu, giúp giảm thời gian tải ban đầu.

jsx
// Lazy loading components
const Dashboard = lazy(() => import('./Dashboard'))
const Settings = lazy(() => import('./Settings'))
const Profile = lazy(() => import('./Profile'))

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
        <Route path="/profile" element={<Profile />} />
      </Routes>
    </Suspense>
  )
}

// Conditional lazy loading
function FeaturePanel({ showAdvanced }) {
  // Component is only loaded if needed
  const AdvancedOptions = lazy(() => import('./AdvancedOptions'))

  return (
    <div>
      <BasicOptions />

      {showAdvanced && (
        <Suspense fallback={<Skeleton />}>
          <AdvancedOptions />
        </Suspense>
      )}
    </div>
  )
}

// Named exports with lazy
const { Chart } = lazy(() =>
  import('./Charts').then(module => ({ default: module.Chart }))
)

18. Cách tối ưu hóa các danh sách dài?

Các danh sách dài có thể gây ra vấn đề về hiệu năng. Virtualization chỉ render những phần tử đang hiển thị trong khung nhìn.

jsx
// With react-window (virtualization library)
import { FixedSizeList } from 'react-window'

function VirtualizedList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style} className="list-item">
      {items[index].name}
    </div>
  )

  return (
    <FixedSizeList
      height={400}
      width="100%"
      itemCount={items.length}
      itemSize={50}
    >
      {Row}
    </FixedSizeList>
  )
}

// Optimization without library: pagination
function PaginatedList({ items, pageSize = 20 }) {
  const [page, setPage] = useState(0)

  const paginatedItems = useMemo(() => {
    const start = page * pageSize
    return items.slice(start, start + pageSize)
  }, [items, page, pageSize])

  const totalPages = Math.ceil(items.length / pageSize)

  return (
    <div>
      <ul>
        {paginatedItems.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>

      <div className="pagination">
        <button
          onClick={() => setPage(p => p - 1)}
          disabled={page === 0}
        >
          Previous
        </button>
        <span>{page + 1} / {totalPages}</span>
        <button
          onClick={() => setPage(p => p + 1)}
          disabled={page >= totalPages - 1}
        >
          Next
        </button>
      </div>
    </div>
  )
}
Ngưỡng áp dụng virtualization

Virtualization trở nên hợp lý khi danh sách có từ vài trăm phần tử trở lên. Với các danh sách nhỏ hơn, pagination hoặc tải dần thường đã đủ.

19. Làm sao tránh các lần render lại không cần thiết?

Việc xác định và loại bỏ các lần render lại không cần thiết là rất quan trọng cho hiệu năng. Có nhiều kỹ thuật giúp tối ưu quá trình render.

jsx
// Technique 1: Separate components
// ❌ Everything re-renders when count changes
function BadExample() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      <ExpensiveComponent /> {/* Unnecessary re-render */}
    </div>
  )
}

// ✅ ExpensiveComponent no longer re-renders
function GoodExample() {
  return (
    <div>
      <Counter />
      <ExpensiveComponent />
    </div>
  )
}

function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

// Technique 2: Pass children instead of rendering directly
// ✅ children is stable, no re-render of children
function ContextProvider({ children }) {
  const [value, setValue] = useState(0)

  return (
    <Context.Provider value={value}>
      {children} {/* children doesn't re-render when value changes */}
    </Context.Provider>
  )
}

// Technique 3: Use DevTools to identify re-renders
// React DevTools > Profiler > "Highlight updates when components render"

20. Automatic batching trong React 18+ là gì?

React 18 tự động gom các cập nhật state để giảm số lần render lại.

jsx
// Before React 18: two re-renders
function handleClick() {
  setCount(c => c + 1) // Re-render
  setFlag(f => !f)      // Re-render
}

// React 18+: single re-render (automatic batching)
function handleClick() {
  setCount(c => c + 1) // Batched
  setFlag(f => !f)      // Batched
  // Single re-render at the end
}

// Even in async callbacks (React 18 novelty)
async function handleSubmit() {
  const response = await fetch('/api/submit')

  // These two updates are batched
  setData(response.data)
  setLoading(false)
  // Single re-render
}

// To force immediate re-render (rare)
import { flushSync } from 'react-dom'

function handleClick() {
  flushSync(() => {
    setCount(c => c + 1)
  })
  // DOM updated here

  flushSync(() => {
    setFlag(f => !f)
  })
  // DOM updated here
}

Sẵn sàng chinh phục phỏng vấn React / Next.js?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

React hiện đại (18+)

21. Giải thích useTransition và useDeferredValue

Hai hook này đánh dấu các cập nhật là không khẩn cấp để giữ giao diện luôn phản hồi nhanh.

jsx
// useTransition: mark an update as non-urgent
function SearchResults() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])
  const [isPending, startTransition] = useTransition()

  const handleChange = (e) => {
    // Urgent update: input stays responsive
    setQuery(e.target.value)

    // Non-urgent update: can be interrupted
    startTransition(() => {
      const filtered = filterLargeDataset(e.target.value)
      setResults(filtered)
    })
  }

  return (
    <div>
      <input value={query} onChange={handleChange} />

      {isPending && <span>Searching...</span>}

      <ul>
        {results.map(r => <li key={r.id}>{r.name}</li>)}
      </ul>
    </div>
  )
}

// useDeferredValue: defer a value
function DeferredSearch() {
  const [query, setQuery] = useState('')
  // deferredQuery is "behind" during rapid updates
  const deferredQuery = useDeferredValue(query)

  // Show indicator during the "lag"
  const isStale = query !== deferredQuery

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />

      <div style={{ opacity: isStale ? 0.5 : 1 }}>
        {/* Uses deferred value, fewer calculations */}
        <ExpensiveList filter={deferredQuery} />
      </div>
    </div>
  )
}

22. Suspense hoạt động ra sao với việc tải dữ liệu?

Suspense quản lý trạng thái tải theo hướng khai báo. Từ React 18+, khả năng này mở rộng sang cả việc tải dữ liệu.

With a compatible library (React Query, Relay, etc.)jsx
function UserProfile({ userId }) {
  return (
    <Suspense fallback={<ProfileSkeleton />}>
      <ProfileContent userId={userId} />
    </Suspense>
  )
}

// The component "suspends" during loading
function ProfileContent({ userId }) {
  // This function suspends if data isn't ready
  const user = useUserData(userId)

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
    </div>
  )
}

// Nested Suspense boundaries
function Dashboard() {
  return (
    <Suspense fallback={<DashboardSkeleton />}>
      <Header />

      <div className="grid">
        <Suspense fallback={<StatsSkeleton />}>
          <Stats />
        </Suspense>

        <Suspense fallback={<ChartSkeleton />}>
          <Chart />
        </Suspense>

        <Suspense fallback={<TableSkeleton />}>
          <RecentActivity />
        </Suspense>
      </div>
    </Suspense>
  )
}

23. useActionState (React 19) là gì?

useActionState (trước đây gọi là useFormState) giúp đơn giản hóa việc xử lý form với Server Actions.

actions.jsjsx
'use server'

async function submitForm(prevState, formData) {
  const email = formData.get('email')
  const password = formData.get('password')

  // Validation
  if (!email || !password) {
    return { error: 'All fields are required' }
  }

  try {
    await createUser({ email, password })
    return { success: true, message: 'Account created!' }
  } catch (e) {
    return { error: e.message }
  }
}

// Component
function SignupForm() {
  const [state, formAction, isPending] = useActionState(submitForm, {})

  return (
    <form action={formAction}>
      {state.error && (
        <div className="error">{state.error}</div>
      )}

      {state.success && (
        <div className="success">{state.message}</div>
      )}

      <input name="email" type="email" placeholder="Email" />
      <input name="password" type="password" placeholder="Password" />

      <button type="submit" disabled={isPending}>
        {isPending ? 'Creating...' : 'Create account'}
      </button>
    </form>
  )
}

24. Server Components hoạt động như thế nào?

Server Components chạy hoàn toàn phía máy chủ, giúp giảm lượng JavaScript gửi xuống client.

ServerComponent.jsxjsx
// No "use client" = Server Component by default
import { prisma } from '@/lib/prisma'

// async/await directly in the component
export default async function ProductList() {
  // Direct database call (no API)
  const products = await prisma.product.findMany({
    orderBy: { createdAt: 'desc' },
    take: 10
  })

  return (
    <div>
      <h2>Recent Products</h2>
      <ul>
        {products.map(product => (
          <li key={product.id}>
            {product.name} - ${product.price}
            {/* Can include Client Components */}
            <AddToCartButton productId={product.id} />
          </li>
        ))}
      </ul>
    </div>
  )
}

// AddToCartButton.jsx
'use client'

// This component needs interactivity
export function AddToCartButton({ productId }) {
  const [isPending, startTransition] = useTransition()

  const handleClick = () => {
    startTransition(async () => {
      await addToCart(productId)
    })
  }

  return (
    <button onClick={handleClick} disabled={isPending}>
      {isPending ? '...' : 'Add'}
    </button>
  )
}

25. Giải thích SSR streaming với React 18

SSR streaming gửi HTML dần dần về trình duyệt thay vì chờ toàn bộ quá trình render hoàn tất.

app/page.jsx (Next.js App Router)jsx
import { Suspense } from 'react'

export default function Page() {
  return (
    <div>
      {/* Immediate render */}
      <Header />

      {/* Streamed when ready */}
      <Suspense fallback={<MainContentSkeleton />}>
        <MainContent />
      </Suspense>

      {/* Streamed independently */}
      <Suspense fallback={<SidebarSkeleton />}>
        <Sidebar />
      </Suspense>

      {/* Immediate render */}
      <Footer />
    </div>
  )
}

// The browser receives:
// 1. HTML shell with Header, skeletons, Footer
// 2. MainContent when its request completes
// 3. Sidebar when its request completes

// loading.jsx for route-level streaming
export default function Loading() {
  return <PageSkeleton />
}

Quản lý state

26. Khi nào nên dùng Redux, Context API hay các giải pháp khác?

Lựa chọn phụ thuộc vào độ phức tạp của state và nhu cầu của dự án.

jsx
// Context API: simple state, few updates
// ✅ Good for: theme, user, preferences
const ThemeContext = createContext()

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

// Redux / Zustand: complex state, frequent updates
// ✅ Good for: cart, filters, complex business data
import { create } from 'zustand'

const useCartStore = create((set, get) => ({
  items: [],
  total: 0,

  addItem: (product) => set((state) => {
    const existing = state.items.find(i => i.id === product.id)
    if (existing) {
      return {
        items: state.items.map(i =>
          i.id === product.id ? { ...i, quantity: i.quantity + 1 } : i
        )
      }
    }
    return { items: [...state.items, { ...product, quantity: 1 }] }
  }),

  removeItem: (id) => set((state) => ({
    items: state.items.filter(i => i.id !== id)
  })),

  getTotal: () => get().items.reduce((sum, i) => sum + i.price * i.quantity, 0)
}))

// React Query / SWR: server state (cache, refetch)
// ✅ Good for: API data, server synchronization
import { useQuery, useMutation } from '@tanstack/react-query'

function Products() {
  const { data, isLoading } = useQuery({
    queryKey: ['products'],
    queryFn: () => fetch('/api/products').then(r => r.json())
  })

  const mutation = useMutation({
    mutationFn: (newProduct) => fetch('/api/products', {
      method: 'POST',
      body: JSON.stringify(newProduct)
    })
  })
}

27. Cách triển khai pattern Reducer?

Pattern Reducer tập trung hóa logic cập nhật state phức tạp.

jsx
// Define actions
const ACTIONS = {
  ADD_TODO: 'ADD_TODO',
  TOGGLE_TODO: 'TOGGLE_TODO',
  DELETE_TODO: 'DELETE_TODO',
  SET_FILTER: 'SET_FILTER'
}

// Pure reducer (no side effects)
function todoReducer(state, action) {
  switch (action.type) {
    case ACTIONS.ADD_TODO:
      return {
        ...state,
        todos: [
          ...state.todos,
          { id: Date.now(), text: action.payload, completed: false }
        ]
      }

    case ACTIONS.TOGGLE_TODO:
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload
            ? { ...todo, completed: !todo.completed }
            : todo
        )
      }

    case ACTIONS.DELETE_TODO:
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.payload)
      }

    case ACTIONS.SET_FILTER:
      return { ...state, filter: action.payload }

    default:
      return state
  }
}

// Usage with useReducer
function TodoApp() {
  const [state, dispatch] = useReducer(todoReducer, {
    todos: [],
    filter: 'all'
  })

  const addTodo = (text) => {
    dispatch({ type: ACTIONS.ADD_TODO, payload: text })
  }

  const toggleTodo = (id) => {
    dispatch({ type: ACTIONS.TOGGLE_TODO, payload: id })
  }

  const filteredTodos = useMemo(() => {
    switch (state.filter) {
      case 'completed':
        return state.todos.filter(t => t.completed)
      case 'active':
        return state.todos.filter(t => !t.completed)
      default:
        return state.todos
    }
  }, [state.todos, state.filter])

  return (
    <div>
      <TodoForm onAdd={addTodo} />
      <TodoList todos={filteredTodos} onToggle={toggleTodo} />
      <FilterButtons
        current={state.filter}
        onChange={(f) => dispatch({ type: ACTIONS.SET_FILTER, payload: f })}
      />
    </div>
  )
}

Kiểm thử

28. Cách kiểm thử một component React?

Các bài test component kiểm tra quá trình render và hành vi của giao diện.

Button.test.jsxjsx
import { render, screen, fireEvent } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Button from './Button'

describe('Button', () => {
  // Basic rendering test
  it('renders with correct text', () => {
    render(<Button>Click me</Button>)
    expect(screen.getByRole('button')).toHaveTextContent('Click me')
  })

  // Interaction test
  it('calls onClick when clicked', async () => {
    const handleClick = jest.fn()
    render(<Button onClick={handleClick}>Click</Button>)

    await userEvent.click(screen.getByRole('button'))

    expect(handleClick).toHaveBeenCalledTimes(1)
  })

  // Disabled state test
  it('does not call onClick when disabled', async () => {
    const handleClick = jest.fn()
    render(<Button disabled onClick={handleClick}>Click</Button>)

    await userEvent.click(screen.getByRole('button'))

    expect(handleClick).not.toHaveBeenCalled()
  })

  // Async state test
  it('shows loading state', async () => {
    render(<Button isLoading>Submit</Button>)

    expect(screen.getByRole('button')).toBeDisabled()
    expect(screen.getByTestId('spinner')).toBeInTheDocument()
  })
})

// Testing a custom hook
import { renderHook, act } from '@testing-library/react'
import useCounter from './useCounter'

describe('useCounter', () => {
  it('increments counter', () => {
    const { result } = renderHook(() => useCounter(0))

    act(() => {
      result.current.increment()
    })

    expect(result.current.count).toBe(1)
  })
})

29. Cách mock các lời gọi API trong kiểm thử?

Mock giúp cô lập bài test khỏi các phụ thuộc bên ngoài.

jsx
// With MSW (Mock Service Worker) - recommended
import { setupServer } from 'msw/node'
import { http, HttpResponse } from 'msw'

const server = setupServer(
  http.get('/api/users', () => {
    return HttpResponse.json([
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' }
    ])
  }),

  http.post('/api/users', async ({ request }) => {
    const body = await request.json()
    return HttpResponse.json({ id: 3, ...body }, { status: 201 })
  })
)

beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

test('loads and displays users', async () => {
  render(<UserList />)

  expect(screen.getByText('Loading...')).toBeInTheDocument()

  await waitFor(() => {
    expect(screen.getByText('Alice')).toBeInTheDocument()
    expect(screen.getByText('Bob')).toBeInTheDocument()
  })
})

// Override for a specific test
test('handles error state', async () => {
  server.use(
    http.get('/api/users', () => {
      return HttpResponse.json({ error: 'Server error' }, { status: 500 })
    })
  )

  render(<UserList />)

  await waitFor(() => {
    expect(screen.getByText('Loading error')).toBeInTheDocument()
  })
})

30. Cách cấu trúc các bài test để đạt độ phủ tối ưu?

Một chiến lược kiểm thử cân bằng sẽ kết hợp nhiều cấp độ test khác nhau.

jsx
// Recommended testing pyramid:
// - Many unit tests (fast, isolated)
// - Some integration tests (components + hooks)
// - Few E2E tests (critical scenarios)

// Unit tests: pure logic
// utils/formatPrice.test.js
describe('formatPrice', () => {
  it('formats with 2 decimals', () => {
    expect(formatPrice(10)).toBe('$10.00')
  })

  it('handles zero', () => {
    expect(formatPrice(0)).toBe('$0.00')
  })
})

// Integration tests: complete component
// features/Checkout/Checkout.test.jsx
describe('Checkout', () => {
  it('completes purchase flow', async () => {
    render(
      <CartProvider>
        <Checkout />
      </CartProvider>
    )

    // Fill the form
    await userEvent.type(screen.getByLabelText('Email'), 'test@example.com')
    await userEvent.type(screen.getByLabelText('Card'), '4242424242424242')

    // Submit
    await userEvent.click(screen.getByRole('button', { name: 'Pay' }))

    // Verify result
    await waitFor(() => {
      expect(screen.getByText('Order confirmed')).toBeInTheDocument()
    })
  })
})

// E2E tests with Playwright
// e2e/checkout.spec.ts
test('user can complete checkout', async ({ page }) => {
  await page.goto('/products')
  await page.click('[data-testid="add-to-cart-1"]')
  await page.click('[data-testid="checkout-button"]')

  await page.fill('[name="email"]', 'test@example.com')
  await page.fill('[name="card"]', '4242424242424242')
  await page.click('button[type="submit"]')

  await expect(page.locator('text=Order confirmed')).toBeVisible()
})

Kết luận

Ba mươi câu hỏi này bao quát những kiến thức React cốt lõi thường gặp trong phỏng vấn. Các mảng quan trọng cần làm chủ:

  • Nền tảng: Virtual DOM, JSX, props vs state, component
  • Hooks: useState, useEffect, useMemo, useCallback, useRef, useContext
  • Pattern: HOC, Render Props, Compound Components, custom hook
  • Hiệu năng: React.memo, lazy loading, virtualization
  • React hiện đại: Suspense, Transitions, Server Components
  • Quản lý state: Context, Redux/Zustand, React Query
  • Kiểm thử: React Testing Library, mock, chiến lược kiểm thử

Việc chuẩn bị phỏng vấn React không chỉ dừng ở việc học thuộc. Thực hành với các dự án thực tế giúp củng cố những khái niệm này và trình bày chúng một cách tự nhiên khi phỏng vấn.

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Thẻ

#react interview
#frontend interview
#react questions
#javascript
#technical interview

Chia sẻ

Bài viết liên quan