30 คำถามสัมภาษณ์ React ที่พบบ่อยที่สุด: คู่มือเตรียมตัวฉบับสมบูรณ์

รวม 30 คำถามสัมภาษณ์ React ที่พบบ่อยที่สุดในปี 2026 พร้อมคำตอบเชิงลึกและตัวอย่างโค้ดเพื่อช่วยให้ผู้สมัครคว้าตำแหน่งนักพัฒนา React

ภาพประกอบคำถามสัมภาษณ์ React พร้อมคอมโพเนนต์และฮุกที่เชื่อมโยงกัน

การสัมภาษณ์เทคนิค React จะวัดความเข้าใจในแนวคิดพื้นฐาน รูปแบบขั้นสูง และแนวปฏิบัติที่ดี คู่มือนี้รวบรวม 30 คำถามที่พบบ่อยที่สุด พร้อมคำตอบเชิงลึกและตัวอย่างโค้ดเพื่อให้การเตรียมตัวเป็นไปอย่างมีประสิทธิภาพ

คำแนะนำในการเตรียมตัว

คำถามเหล่านี้ถูกจัดเรียงตามระดับความยาก การฝึกฝนพื้นฐานให้แม่นยำก่อนเข้าสู่แนวคิดขั้นสูงจะช่วยให้การเตรียมตัวมีโครงสร้างที่ชัดเจนยิ่งขึ้น

พื้นฐานของ React

1. Virtual DOM คืออะไรและทำไม React ถึงใช้

Virtual DOM คือการแทนค่าของ DOM จริงในรูปแบบ JavaScript ที่มีน้ำหนักเบา React ใช้สิ่งนี้เพื่อปรับแต่งการอัปเดตอินเทอร์เฟซให้มีประสิทธิภาพ

กระบวนการประกอบด้วยสามขั้นตอน ได้แก่ React สร้างสำเนาเสมือนของ DOM ก่อน จากนั้นเปรียบเทียบสำเนาดังกล่าวกับเวอร์ชันก่อนหน้าเมื่อมีการเปลี่ยนแปลง (อัลกอริทึม diffing) และสุดท้ายนำเฉพาะการเปลี่ยนแปลงที่จำเป็นไปใช้กับ DOM จริง (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>
  )
}

วิธีการนี้หลีกเลี่ยงการดำเนินการที่มีต้นทุนสูงบน DOM และทำให้สามารถอัปเดตอินเทอร์เฟซที่ซับซ้อนได้อย่างรวดเร็ว

2. คอมโพเนนต์แบบฟังก์ชันกับแบบคลาสต่างกันอย่างไร

คอมโพเนนต์แบบฟังก์ชันคือฟังก์ชัน JavaScript ที่รับ props และคืนค่าเป็น JSX นับตั้งแต่ React 16.8 เป็นต้นมา ฮุกอนุญาตให้ใช้ state และ lifecycle ในคอมโพเนนต์แบบฟังก์ชันได้

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>
  }
}

ปัจจุบันคอมโพเนนต์แบบฟังก์ชันถือเป็นมาตรฐาน คอมโพเนนต์แบบคลาสยังคงรองรับอยู่แต่ไม่แนะนำสำหรับโปรเจกต์ใหม่

3. JSX ทำงานอย่างไร

JSX คือส่วนขยายไวยากรณ์ของ JavaScript ที่อนุญาตให้เขียนมาร์กอัปภายในโค้ด JSX ไม่ใช่ HTML แต่เป็น JavaScript ในรูปแบบที่อ่านง่ายกว่า

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')
)

ความแตกต่างจาก HTML ได้แก่ การใช้ className แทน class, htmlFor แทน for, การใช้ camelCase สำหรับแอตทริบิวต์ (onClick, tabIndex) และต้องปิดแท็กที่ปิดในตัวเองเสมอ

4. state กับ props ต่างกันอย่างไร

Props คือข้อมูลที่ส่งจากคอมโพเนนต์แม่ไปยังลูก เป็นข้อมูลที่อ่านได้อย่างเดียว ส่วน state คือสถานะภายในของคอมโพเนนต์ที่สามารถเปลี่ยนแปลงผ่าน 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" />

หลักการพื้นฐานคือ props ไหลจากบนลงล่าง (พ่อแม่ไปยังลูก) ส่วน state เป็นของแต่ละคอมโพเนนต์โดยเฉพาะ

5. ทำไม key ในรายการจึงสำคัญ

Key ช่วยให้ React ระบุได้ว่ามีองค์ประกอบใดเปลี่ยนแปลง เพิ่ม หรือถูกลบออกจากรายการ หากไม่มี key ที่ไม่ซ้ำและคงที่ React อาจแสดงพฤติกรรมที่ไม่คาดคิด

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. อธิบาย useState และข้อผิดพลาดที่พบบ่อย

useState ใช้จัดการ state ภายในคอมโพเนนต์แบบฟังก์ชัน setter รับได้ทั้งค่าใหม่หรือฟังก์ชันสำหรับอัปเดต

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 ทำงานอย่างไรกับอาร์เรย์ของ dependency

useEffect ทำงาน side effect หลังจากการเรนเดอร์ อาร์เรย์ของ dependency ควบคุมว่าเอฟเฟกต์จะถูกเรียกเมื่อใด

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)
}, [])
กฎของ ESLint

ควรเปิดใช้งาน eslint-plugin-react-hooks เสมอเพื่อตรวจจับ dependency ที่ขาดหายไป กฎนี้ช่วยป้องกันบั๊กที่วินิจฉัยได้ยากจำนวนมาก

8. เมื่อใดควรใช้ useMemo และ useCallback

ฮุกทั้งสองนี้ใช้สำหรับ memoization เพื่อหลีกเลี่ยงการคำนวณซ้ำหรือการสร้างค่าใหม่โดยไม่จำเป็น ควรระวังไม่ใช้มากเกินไป

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>
}

ควรใช้ฮุกเหล่านี้เฉพาะเมื่อพบปัญหาด้านประสิทธิภาพที่ชัดเจน หรือเมื่อต้องการรักษา reference ที่ส่งให้กับคอมโพเนนต์ที่ถูก memoize ไว้ให้คงที่

9. useRef ทำงานอย่างไรและใช้ในกรณีใดบ้าง

useRef สร้าง reference ที่เปลี่ยนแปลงได้และคงอยู่ระหว่างการเรนเดอร์ โดยไม่ทำให้เกิดการเรนเดอร์ใหม่เมื่อมีการเปลี่ยนแปลงค่า

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. อธิบาย useContext และเมื่อใดควรใช้

useContext ช่วยให้เข้าถึง context ของ React โดยไม่ต้องส่ง props ทอดต่อกันหลายชั้น เหมาะสำหรับข้อมูลแบบ global เช่น ธีมหรือผู้ใช้ที่ล็อกอินอยู่

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>
  )
}

พร้อมที่จะพิชิตการสัมภาษณ์ React / Next.js แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

รูปแบบขั้นสูง

11. Higher-Order Component (HOC) คืออะไร

HOC คือฟังก์ชันที่รับคอมโพเนนต์เข้ามาและคืนค่าเป็นคอมโพเนนต์ใหม่ที่ถูกเสริมความสามารถ ใช้น้อยลงตั้งแต่มีฮุก แต่ยังคงพบได้ในไลบรารีบางตัว

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. อธิบายรูปแบบ Render Props

รูปแบบ Render Props ช่วยแบ่งปันลอจิกระหว่างคอมโพเนนต์โดยส่งผ่าน prop ที่เป็นฟังก์ชัน

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. วิธีสร้าง custom hook

Custom hook ใช้สำหรับแยกและนำลอจิกที่มี state ไปใช้ร่วมกันระหว่างคอมโพเนนต์ต่างๆ

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 คืออะไร

รูปแบบนี้ช่วยสร้างคอมโพเนนต์ที่ทำงานร่วมกันโดยปริยาย คล้ายกับการใช้แท็ก <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. วิธีนำรูปแบบ Controlled vs Uncontrolled ไปใช้

คอมโพเนนต์แบบ controlled มี state ที่ถูกจัดการโดย React ส่วนแบบ uncontrolled ใช้ 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}
    />
  )
}

ประสิทธิภาพและการเพิ่มประสิทธิภาพ

16. React.memo ทำงานอย่างไร

React.memo คือ HOC ที่ทำการ memoize คอมโพเนนต์เพื่อหลีกเลี่ยงการเรนเดอร์ใหม่หาก props ไม่เปลี่ยนแปลง

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 และ React.lazy คืออะไร

Code splitting แบ่งบันเดิลออกเป็น chunk ที่โหลดตามต้องการ ช่วยลดเวลาในการโหลดครั้งแรก

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. วิธีปรับแต่งรายการที่มีความยาวมาก

รายการที่ยาวมากอาจทำให้เกิดปัญหาด้านประสิทธิภาพ การทำ virtualization จะเรนเดอร์เฉพาะองค์ประกอบที่มองเห็นได้

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>
  )
}
เกณฑ์การใช้ virtualization

Virtualization จะเริ่มมีประโยชน์เมื่อรายการมีองค์ประกอบเกินไม่กี่ร้อยรายการ สำหรับรายการที่เล็กกว่านั้น การใช้ pagination หรือการโหลดแบบทยอยมักเพียงพอแล้ว

19. วิธีหลีกเลี่ยงการเรนเดอร์ซ้ำที่ไม่จำเป็น

การระบุและกำจัดการเรนเดอร์ซ้ำที่ไม่จำเป็นเป็นสิ่งสำคัญต่อประสิทธิภาพ มีเทคนิคหลายอย่างที่ช่วยปรับการเรนเดอร์ได้

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 ใน React 18+ คืออะไร

React 18 จะรวมการอัปเดต state โดยอัตโนมัติเพื่อลดจำนวนการเรนเดอร์ซ้ำ

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
}

พร้อมที่จะพิชิตการสัมภาษณ์ React / Next.js แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

React สมัยใหม่ (18+)

21. อธิบาย useTransition และ useDeferredValue

ฮุกทั้งสองนี้ใช้สำหรับทำเครื่องหมายว่าอัปเดตใดเป็นแบบไม่เร่งด่วน เพื่อรักษาให้อินเทอร์เฟซตอบสนองได้รวดเร็ว

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 สำหรับการดึงข้อมูลทำงานอย่างไร

Suspense จัดการสถานะการโหลดในรูปแบบเชิงประกาศ และตั้งแต่ React 18+ สามารถใช้กับการดึงข้อมูลได้

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) คืออะไร

useActionState (ชื่อเดิม useFormState) ช่วยให้การจัดการฟอร์มร่วมกับ Server Action ง่ายขึ้น

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 Component ทำงานอย่างไร

Server Component ทำงานบนเซิร์ฟเวอร์เท่านั้น ทำให้ลดปริมาณ JavaScript ที่ส่งไปยังไคลเอนต์ได้

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. อธิบาย SSR streaming ใน React 18

SSR streaming จะส่ง HTML ไปยังเบราว์เซอร์อย่างค่อยเป็นค่อยไป แทนที่จะรอให้การเรนเดอร์เสร็จสมบูรณ์ก่อน

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 />
}

การจัดการ state

26. เมื่อใดควรใช้ Redux, Context API หรือทางเลือกอื่น

การเลือกขึ้นอยู่กับความซับซ้อนของ state และความต้องการของโปรเจกต์

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. วิธีนำรูปแบบ Reducer ไปใช้

รูปแบบ Reducer ใช้รวมศูนย์ลอจิกการอัปเดต state ที่ซับซ้อนไว้ในที่เดียว

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>
  )
}

การทดสอบ

28. วิธีทดสอบคอมโพเนนต์ React

การทดสอบคอมโพเนนต์ใช้สำหรับตรวจสอบการเรนเดอร์และพฤติกรรมของอินเทอร์เฟซ

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. วิธี mock การเรียก API ในการทดสอบ

Mock ช่วยแยกการทดสอบออกจากการพึ่งพาภายนอก

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. วิธีจัดโครงสร้างการทดสอบเพื่อความครอบคลุมที่เหมาะสม

กลยุทธ์การทดสอบที่สมดุลควรรวมการทดสอบในระดับต่างๆ เข้าด้วยกัน

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()
})

บทสรุป

คำถามทั้ง 30 ข้อนี้ครอบคลุมความรู้ React ที่จำเป็นสำหรับการสัมภาษณ์ ขอบเขตหลักที่ควรเชี่ยวชาญ ได้แก่

  • พื้นฐาน: Virtual DOM, JSX, props กับ state, คอมโพเนนต์
  • Hooks: useState, useEffect, useMemo, useCallback, useRef, useContext
  • รูปแบบ: HOC, Render Props, Compound Components, custom hooks
  • ประสิทธิภาพ: React.memo, lazy loading, virtualization
  • React สมัยใหม่: Suspense, Transitions, Server Components
  • การจัดการ state: Context, Redux/Zustand, React Query
  • การทดสอบ: React Testing Library, mocks, กลยุทธ์การทดสอบ

การเตรียมตัวสัมภาษณ์ React ไม่ได้เป็นเพียงการท่องจำ การฝึกฝนจากโปรเจกต์จริงช่วยให้แนวคิดเหล่านี้ฝังแน่น และอธิบายได้อย่างเป็นธรรมชาติในระหว่างการสัมภาษณ์

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

แท็ก

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

แชร์

บทความที่เกี่ยวข้อง