Топ 30 питань на співбесіді з React: повний посібник для успіху

30 найчастіших питань на співбесіді з React у 2026 році. Детальні відповіді, приклади коду та поради для отримання роботи React-розробника.

Ілюстрація питань співбесіди з React із взаємопов'язаними компонентами та хуками

Технічні співбесіди з React перевіряють розуміння фундаментальних концепцій, просунутих патернів та найкращих практик. Цей посібник містить 30 найчастіших питань із детальними відповідями та прикладами коду для ефективної підготовки.

Порада щодо підготовки

Ці питання впорядковані за рівнем складності. Опанування основ перед переходом до складних концепцій дозволяє побудувати більш послідовну підготовку.

Основи React

1. Що таке Virtual DOM і чому React його використовує?

Virtual DOM — це легке JavaScript-представлення реального DOM. React використовує цю абстракцію, щоб оптимізувати оновлення інтерфейсу.

Процес складається з трьох кроків: React спочатку створює віртуальну копію DOM, потім порівнює цю копію з попередньою версією під час змін (алгоритм diffing), і нарешті застосовує до реального DOM лише необхідні модифікації (реконсиляція).

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, хуки дозволяють використовувати стан та життєвий цикл у функціональних компонентах.

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, яке дозволяє писати розмітку безпосередньо в коді. Це не 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 — це внутрішній стан компонента, що змінюється через сетери.

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. Чому ключі важливі у списках?

Ключі допомагають React визначити, які елементи змінилися, додалися або були видалені у списку. Без унікальних і стабільних ключів 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 керує локальним станом у функціональному компоненті. Сетер може приймати значення або функцію оновлення.

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 зі своїм масивом залежностей?

useEffect виконує побічні ефекти після рендеру. Масив залежностей контролює, коли ефект спрацьовує.

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 для виявлення відсутніх залежностей. Це правило запобігає багатьом важко діагностованим помилкам.

8. Коли використовувати useMemo та useCallback?

Ці хуки дозволяють мемоізувати обчислення та функції, щоб уникнути непотрібних повторних обчислень або створень. Їх не варто використовувати надмірно.

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

Ці хуки слід застосовувати лише тоді, коли виявлено реальну проблему продуктивності, або для стабілізації посилань, що передаються у мемоізовані компоненти.

9. Як працює useRef і які його випадки використання?

useRef створює змінне посилання, що зберігається між рендерами без запуску повторного рендеру при зміні.

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 дає доступ до React-контексту без передачі пропсів через усі рівні. Підходить для глобальних даних на кшталт теми або поточного користувача.

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 дозволяє ділитися логікою між компонентами через проп, що є функцією.

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. Як створити власний хук?

Власні хуки дозволяють виокремлювати та повторно використовувати логіку зі станом між компонентами.

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) компонентах станом керує 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, який мемоізує компонент, щоб уникнути повторних рендерів, коли пропси не змінилися.

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 ділить бандл на частини, які завантажуються за потреби, зменшуючи час початкового завантаження.

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. Як оптимізувати довгі списки?

Довгі списки можуть спричиняти проблеми з продуктивністю. Віртуалізація рендерить лише видимі елементи.

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>
  )
}
Поріг віртуалізації

Віртуалізація стає доречною, коли йдеться про кілька сотень елементів і більше. Для менших списків часто достатньо пагінації або поступового завантаження.

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. Що таке автоматичний батчинг у React 18+?

React 18 автоматично групує оновлення стану, щоб зменшити кількість повторних рендерів.

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

Server Components виконуються виключно на сервері, зменшуючи обсяг 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 />
}

Керування станом

26. Коли використовувати Redux, Context API чи інші рішення?

Вибір залежить від складності стану та потреб проєкту.

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 централізує складну логіку оновлення стану.

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. Як мокати API-запити в тестах?

Моки ізолюють тести від зовнішніх залежностей.

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, компоненти
  • Хуки: useState, useEffect, useMemo, useCallback, useRef, useContext
  • Патерни: HOC, Render Props, Compound Components, власні хуки
  • Продуктивність: React.memo, lazy loading, віртуалізація
  • Сучасний React: Suspense, Transitions, Server Components
  • Керування станом: Context, Redux/Zustand, React Query
  • Тестування: React Testing Library, моки, стратегія тестування

Підготовка до співбесіди з React — це не лише заучування. Практика на реальних проєктах допомагає закріпити ці концепції та природно пояснювати їх під час інтерв'ю.

Починай практикувати!

Перевір свої знання з нашими симуляторами співбесід та технічними тестами.

Теги

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

Поділитися

Пов'язані статті