Irbisa · Шпаргалка

Паттерны и архитектура

Паттерны композиции, compound components, render props и HOC, error boundaries, порталы, структура папок, дизайн-системы, монорепозитории

Senior-разработчик · 20 вопросов

  1. 01

    Полезно ли ещё разделение на контейнерные и презентационные компоненты, когда есть хуки?

    Сама идея (держать рендер отдельно от данных и сайд-эффектов) по-прежнему верна, но теперь граница обычно проходит между кастомным хуком и компонентом, который рендерит, а не между двумя слоями компонентов.

  2. 02

    Как принципы SOLID применяются к React-компонентам и хукам?

    SOLID придумали для классов, но у каждого правила есть React-версия: одна причина для изменения у компонента или хука, расширение через композицию вместо новых флагов, обёртки, которые сохраняют контракт того, что оборачивают, узкие пропсы и зависимости, переданные снаружи, а не импортированные.

    import type { ReactNode } from 'react'; // I: ask for the fields you use, not the whole `user`function Avatar({ name, src }: { name: string; src: string }) {  return <img src={src} alt={name} className="avatar" />;}…
  3. 03

    Как сделать внедрение зависимостей (DI) в React, например чтобы подменить API-клиент в тестах или Storybook?

    В React нет DI-контейнера: зависимости внедряют через пропсы, контекст и систему модулей.

    import { createContext, use, type ReactNode } from 'react'; interface Api {  getUser(id: string): Promise<User>;  saveUser(user: User): Promise<void>;}…
  4. 04

    Как избавиться от prop drilling, не перенося всё в глобальное состояние?

    Сначала перестроить дерево: передавать готовые элементы через children или слоты, чтобы промежуточные компоненты не прокидывали ненужные им пропсы.

    import type { ReactNode } from 'react'; // before: Page -> Layout -> Sidebar -> UserMenu all forward `user`// after: Layout only places what it is givenfunction Layout({ sidebar, children }: { sidebar: ReactNode; children: ReactNode }) {  return (…
  5. 05

    Что такое compound components и как построить такой компонент на контексте?

    Compound components представляют собой набор компонентов, которые работают только вместе и делят неявное состояние через контекст родителя, например <Tabs>, <Tabs.Tab> и <Tabs.Panel>.

    import { createContext, use, useState, type ReactNode } from 'react'; const TabsContext = createContext<{ active: string; setActive: (id: string) => void } | null>(null); function useTabs() {  const ctx = use(TabsContext);…
  6. 06

    Что такое render prop и когда его всё ещё стоит выбрать вместо кастомного хука?

    Render prop: проп-функция, которую компонент вызывает со своим внутренним состоянием, чтобы узнать, что рендерить, например renderItem={(item) => ...}.

    import { useState, type ReactNode } from 'react'; interface SelectProps<T> {  items: T[];  getKey: (item: T) => string;  renderOption: (item: T, state: { selected: boolean }) => ReactNode;…
  7. 07

    Что такое higher-order component (HOC), какие у него проблемы и что его заменило?

    HOC (higher-order component): функция, которая принимает компонент и возвращает новый с дополнительными пропсами или поведением, например withAuth(Page) или старый connect из Redux.

    import type { ComponentType } from 'react';import { Navigate } from 'react-router'; function withAuth<P extends object>(Wrapped: ComponentType<P>) {  function WithAuth(props: P) {    const { user, isLoading } = useAuth();…
  8. 08

    Что такое headless-компонент или headless UI-библиотека (Radix, React Aria, TanStack Table) и зачем строить на ней интерфейс?

    Headless-компонент или библиотека даёт поведение без разметки и стилей: состояние, работу с клавиатурой, управление фокусом и ARIA-связи, а каждый элемент рендерите и стилизуете вы.

    import { useId, useState } from 'react'; // Behaviour and ARIA only: no markup, no stylesfunction useDisclosure(initialOpen = false) {  const [open, setOpen] = useState(initialOpen);  const panelId = useId();…
  9. 09

    Что такое error boundary и какие ошибки он не ловит?

    Error boundary ловит ошибки, брошенные при рендере его поддерева (и в методах жизненного цикла), и показывает fallback вместо размонтирования всего приложения.

    import { Component, type ErrorInfo, type ReactNode } from 'react'; interface Props { fallback: ReactNode; children: ReactNode }interface State { hasError: boolean } class ErrorBoundary extends Component<Props, State> {…
  10. 10

    Найдите баг: когда api.save() падает, fallback error boundary так и не появляется, а в консоли видно необработанный reject промиса.

    function SaveButton() {  async function handleClick() {    await api.save(); // rejects when the server is down  }  return <button onClick={handleClick}>Save</button>;} function Editor() {  return (    <ErrorBoundary fallback={<p>Something went wrong</p>}>      <SaveButton />    </ErrorBoundary>  );}

    Error boundary ловит ошибки, брошенные во время рендера, а обработчик события (тем более промис, который отклоняется позже) выполняется вне рендера, поэтому reject уходит в unhandledrejection, а UI продолжает работать как ни в чём не бывало.

  11. 11

    Что такое портал и когда он нужен?

    createPortal(children, domNode) рендерит дочерние элементы в другой DOM-узел (обычно в конец document.body), при этом в дереве React они остаются на своём месте.

    import type { ReactNode } from 'react';import { createPortal } from 'react-dom'; interface ModalProps {  open: boolean;  onClose: () => void;…
  12. 12

    Что выведется в консоль, когда пользователь нажмёт на кнопку, отрендеренную через портал?

    import { createPortal } from 'react-dom'; function App() {  return (    <div onClick={() => console.log('App div')}>      <Popup />    </div>  );} function Popup() {  return createPortal(    <button onClick={() => console.log('button')}>Open</button>,    document.body,  );}

    Выведется button, затем App div: React распространяет свои события по дереву React, а там содержимое портала является потомком div, хотя в DOM кнопка лежит прямо в document.body.

  13. 13

    Как бы вы организовали папки в большом React-приложении?

    Группировать код по фичам, а не по типу файлов: features/checkout держит вместе свои компоненты, хуки, запросы к API и тесты, а shared/ хранит общий UI и утилиты, которые ничего не знают о фичах.

    src/  app/              providers, router, global styles  pages/            route components: compose features, no logic  features/    checkout/      api.ts…
  14. 14

    Проведите код-ревью этого компонента: какие проблемы вы видите и в каком порядке о них скажете?

    function TeamMembers({ teamId, onSelect }: { teamId: string; onSelect: (user: User) => void }) {  const [users, setUsers] = useState<User[]>([]);  const [query, setQuery] = useState('');  const [visible, setVisible] = useState<User[]>([]);   useEffect(() => {    fetch(`/api/teams/${teamId}/users`).then((res) => res.json()).then(setUsers);  }, []);  useEffect(() => setVisible(users.filter((u) => u.name.includes(query))), [users, query]);  return (    <div>      <input value={query} onChange={(e) => setQuery(e.target.value)} />      {visible.map((u) => <div key={u.id} onClick={() => onSelect(u)}>{u.name}</div>)}    </div>  );}

    Называйте проблемы по степени влияния: сначала баги (эффект игнорирует teamId, не делает cleanup и доверяет любому ответу), потом производное состояние в эффекте, потом доступность и только затем нейминг и стиль.

  15. 15

    Как спроектировать переиспользуемый компонент, который работает и как контролируемый, и как неконтролируемый?

    Следовать контракту нативного инпута: принимать value с onChange для контролируемого режима и defaultValue для неконтролируемого, держать внутренний стейт только пока value равен undefined и вызывать onChange в обоих режимах.

    import { useState, type ReactNode } from 'react'; function useControllableState<T>(value: T | undefined, defaultValue: T, onChange?: (next: T) => void) {  const [inner, setInner] = useState(defaultValue);  const isControlled = value !== undefined;  const current = isControlled ? value : inner;…
  16. 16

    Где ставить error boundary в приложении и как они работают вместе с Suspense и повтором?

    Одну в корне как последний рубеж, по одной на роут, чтобы сломанная страница не убивала навигацию, и вокруг каждого виджета, который может упасть сам по себе (график, блок комментариев).

    import { Suspense, type ReactNode } from 'react';import { ErrorBoundary } from 'react-error-boundary';import { useLocation } from 'react-router'; function RouteShell({ children }: { children: ReactNode }) {  const { pathname } = useLocation();…
  17. 17

    Как настроить мониторинг ошибок в продакшен-приложении на React, чтобы отчёты были читаемыми и по ним можно было действовать?

    Ловите ошибки из всех источников, привязывайте каждый отчёт к вашему коду и релизу, отсекайте шум и шлите алерты о новом и растущем, а не о каждом событии.

    import * as Sentry from '@sentry/react';import { createRoot } from 'react-dom/client'; Sentry.init({  dsn: import.meta.env.VITE_SENTRY_DSN,  release: import.meta.env.VITE_GIT_SHA, // ties every event to one deploy…
  18. 18

    Как не дать фичам импортировать внутренности друг друга и в чём подвохи barrel-файлов?

    Дать каждой фиче одну публичную точку входа (features/cart/index.ts) и закрепить это правилом линтера, которое запрещает глубокие импорты вроде features/cart/ui/CartRow.

    // eslint.config.jsexport default [  {    files: ['src/**/*.{ts,tsx}'],    rules: {      'no-restricted-imports': ['error', {…
  19. 19

    Когда монорепозиторий оправдан для React-проектов и как его настроить?

    Монорепозиторий окупается, когда несколько приложений делят код (дизайн-систему, API-клиент, конфиги) и нужно одним PR поменять библиотеку и всех её потребителей сразу.

    {  "name": "@acme/ui",  "version": "0.0.0",  "private": true,  "type": "module",  "exports": {…
  20. 20

    Как бы вы перевели большое приложение с React 18 на React 19 и что при этом обычно ломается?

    Сначала перейдите на React 18.3: он предупреждает о каждом API, удалённом в React 19.

    # 1. React 18.3 is 18.2 plus a warning for every API React 19 removesnpm install react@18.3 react-dom@18.3# run the app and the tests, fix every warning # 2. Codemods for the removed APIs and for the type changesnpx codemod@latest react/19/migration-recipe…