Хотите заказать веб-сайт? Связаться с нами

High Order Components — фабрика крутых компонентов в React

Цель урока: понять паттерн Higher Order Components (HOC) — функцию, которая принимает компонент и возвращает новый компонент с дополнительной логикой. Научиться создавать HOC для авторизации, логирования, загрузки данных, обработки ошибок и других сквозных задач.

High Order Components — фабрика крутых компонентов в React

Введение: что такое High Order Component

HOC (Higher Order Component) — это функция, которая принимает компонент и возвращает новый компонент с добавленной функциональностью.

Главная мысль: HOC — это как «обёртка» для компонента. Ты берёшь обычный компонент, кладёшь его в HOC, и на выходе получаешь компонент с новыми возможностями.

Почему HOC?

  1. Переиспользование логики — одна и та же логика для многих компонентов
  2. Сквозные задачи (cross-cutting concerns) — авторизация, логирование, загрузка
  3. Разделение ответственности — компонент занимается интерфейсом, HOC — логикой
  4. Альтернатива Render Props (в некоторых случаях)

Важно: HOC — это не часть React API, это паттерн, основанный на возможностях React.

Базовый пример HOC

Проблема: нужно добавить логирование рендеров во все компоненты.

Решение через HOC:

// HOC — добавляет логирование
function withLogger(WrappedComponent) {
  // Возвращаем новый компонент
  return function WithLogger(props) {
    console.log(`Компонент ${WrappedComponent.name} рендерится`);
    console.log('Пропсы:', props);
    
    // Рендерим оригинальный компонент с его пропсами
    return <WrappedComponent {...props} />;
  };
}

// Обычный компонент
function UserProfile({ name, age }) {
  return (
    <div>
      <h2>{name}</h2>
      <p>Возраст: {age}</p>
    </div>
  );
}

// Оборачиваем компонент в HOC
const UserProfileWithLogger = withLogger(UserProfile);

// Использование
function App() {
  return <UserProfileWithLogger name="Алексей" age={25} />;
}
// В консоли: 
// Компонент UserProfile рендерится
// Пропсы: { name: "Алексей", age: 25 }

Что здесь происходит:

  1. withLogger — функция, принимающая компонент
  2. Возвращает новый компонент WithLogger
  3. WithLogger рендерит оригинальный компонент с теми же пропсами
  4. Добавляет логирование перед рендером

HOC для авторизации (проверка прав)

// HOC для проверки авторизации
function withAuth(WrappedComponent) {
  return function WithAuth(props) {
    const isAuthenticated = localStorage.getItem('token') !== null;
    
    if (!isAuthenticated) {
      return <p>❌ Пожалуйста, войдите в систему</p>;
    }
    
    // Если авторизован — показываем компонент
    return <WrappedComponent {...props} />;
  };
}

// Компонент, требующий авторизации
function Dashboard({ user }) {
  return (
    <div>
      <h1>Панель управления</h1>
      <p>Добро пожаловать, {user.name}!</p>
    </div>
  );
}

// Оборачиваем
const ProtectedDashboard = withAuth(Dashboard);

function App() {
  return <ProtectedDashboard user={{ name: 'Алексей' }} />;
}

HOC для загрузки данных

// HOC для загрузки данных с сервера
function withData(WrappedComponent, url) {
  return function WithData(props) {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
      const fetchData = async () => {
        try {
          const response = await fetch(url);
          if (!response.ok) throw new Error('Ошибка загрузки');
          const result = await response.json();
          setData(result);
        } catch (err) {
          setError(err.message);
        } finally {
          setLoading(false);
        }
      };

      fetchData();
    }, [url]);

    if (loading) return <p>Загрузка...</p>;
    if (error) return <p>Ошибка: {error}</p>;

    // Передаём данные в компонент через пропс data
    return <WrappedComponent {...props} data={data} />;
  };
}

// Компонент для отображения пользователей
function UserList({ data, title }) {
  return (
    <div>
      <h2>{title}</h2>
      <ul>
        {data.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

// Оборачиваем
const UserListWithData = withData(
  UserList, 
  'https://api.example.com/users'
);

function App() {
  return <UserListWithData title="Список пользователей" />;
}
Из ноутбука появляются летающие светящиеся экраны

Оживи свой сайт. Освой JavaScript!

Статичная верстка — это только скелет. Наш онлайн-курс "JavaScript с нуля до профи" даст твоим страницам мышцы и нервы. Научись создавать слайдеры, формы, интерактивные карты и получать данные с сервера.

От теории — к реальным скриптам в твоём портфолио.

Подробнее о курсе

HOC с конфигурацией (фабрика HOC)

// Фабрика HOC с параметрами
function withStyles(styles) {
  return function withStylesHOC(WrappedComponent) {
    return function WithStyles(props) {
      const style = {
        border: '2px solid ' + styles.borderColor,
        borderRadius: '8px',
        padding: '16px',
        background: styles.background || 'white',
        margin: '10px 0'
      };

      return (
        <div style={style}>
          <WrappedComponent {...props} />
        </div>
      );
    };
  };
}

// Создаём HOC с разными стилями
const withRedBorder = withStyles({ borderColor: 'red', background: '#fff0f0' });
const withBlueBorder = withStyles({ borderColor: 'blue', background: '#f0f4ff' });
const withGreenBorder = withStyles({ borderColor: 'green', background: '#f0fff0' });

// Использование
function SimpleCard({ title, content }) {
  return (
    <div>
      <h3>{title}</h3>
      <p>{content}</p>
    </div>
  );
}

const RedCard = withRedBorder(SimpleCard);
const BlueCard = withBlueBorder(SimpleCard);
const GreenCard = withGreenBorder(SimpleCard);

function App() {
  return (
    <div>
      <RedCard title="Ошибка" content="Что-то пошло не так" />
      <BlueCard title="Информация" content="Обновление доступно" />
      <GreenCard title="Успех" content="Операция выполнена" />
    </div>
  );
}

Композиция HOC (несколько обёрток)

Можно применять несколько HOC к одному компоненту:

// HOC для логирования
function withLogger(WrappedComponent) {
  return function WithLogger(props) {
    console.log(`Рендер ${WrappedComponent.name}`);
    return <WrappedComponent {...props} />;
  };
}

// HOC для авторизации
function withAuth(WrappedComponent) {
  return function WithAuth(props) {
    const isAuthenticated = localStorage.getItem('token') !== null;
    if (!isAuthenticated) {
      return <p>Требуется авторизация</p>;
    }
    return <WrappedComponent {...props} />;
  };
}

// HOC для загрузки
function withLoading(WrappedComponent) {
  return function WithLoading(props) {
    const [loading, setLoading] = useState(true);
    
    useEffect(() => {
      setTimeout(() => setLoading(false), 1000);
    }, []);

    if (loading) return <p>Загрузка...</p>;
    return <WrappedComponent {...props} />;
  };
}

// Базовый компонент
function UserProfile({ name, age }) {
  return (
    <div>
      <h2>{name}</h2>
      <p>Возраст: {age}</p>
    </div>
  );
}

// Композиция HOC
const EnhancedUserProfile = withLogger(
  withAuth(
    withLoading(UserProfile)
  )
);

// Или с помощью композиции функций
const compose = (...fns) => (component) => 
  fns.reduceRight((acc, fn) => fn(acc), component);

const EnhancedUserProfile2 = compose(
  withLogger,
  withAuth,
  withLoading
)(UserProfile);

function App() {
  return <EnhancedUserProfile name="Алексей" age={25} />;
}

HOC для обработки ошибок

// HOC для обработки ошибок
function withErrorBoundary(WrappedComponent, fallbackComponent) {
  return class ErrorBoundary extends React.Component {
    constructor(props) {
      super(props);
      this.state = { hasError: false, error: null };
    }

    static getDerivedStateFromError(error) {
      return { hasError: true, error };
    }

    componentDidCatch(error, errorInfo) {
      console.error('Ошибка в компоненте:', error, errorInfo);
    }

    render() {
      if (this.state.hasError) {
        if (fallbackComponent) {
          return fallbackComponent({ error: this.state.error });
        }
        return (
          <div style={{ color: 'red', padding: '20px' }}>
            <h3>Что-то пошло не так</h3>
            <p>{this.state.error?.message}</p>
          </div>
        );
      }

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

// Использование
function BuggyComponent({ shouldCrash }) {
  if (shouldCrash) {
    throw new Error('Компонент упал!');
  }
  return <p>Компонент работает нормально</p>;
}

const SafeComponent = withErrorBoundary(
  BuggyComponent,
  ({ error }) => (
    <div style={{ color: 'orange' }}>
      ⚠️ Компонент временно недоступен: {error.message}
    </div>
  )
);

function App() {
  const [crash, setCrash] = useState(false);

  return (
    <div>
      <button onClick={() => setCrash(true)}>
        Сломать компонент
      </button>
      <SafeComponent shouldCrash={crash} />
    </div>
  );
}

Передача ref через HOC (React.forwardRef)

// HOC с forwardRef
function withFocus(WrappedComponent) {
  function WithFocus(props, ref) {
    const focus = () => {
      if (ref) {
        ref.current?.focus();
      }
    };

    return (
      <div>
        <button onClick={focus}>Фокус на поле</button>
        <WrappedComponent {...props} ref={ref} />
      </div>
    );
  }

  return React.forwardRef(WithFocus);
}

// Базовый компонент
const InputField = forwardRef((props, ref) => (
  <input ref={ref} {...props} />
));

// Оборачиваем
const FocusableInput = withFocus(InputField);

function App() {
  const inputRef = useRef(null);

  return (
    <div>
      <FocusableInput 
        ref={inputRef} 
        placeholder="Введите текст"
      />
    </div>
  );
}

HOC для мемоизации (оптимизация)

// HOC для мемоизации компонента
function withMemo(WrappedComponent) {
  return React.memo(WrappedComponent);
}

// Использование
const UserCard = withMemo(({ user }) => {
  console.log('Рендер UserCard');
  return (
    <div>
      <h3>{user.name}</h3>
      <p>{user.email}</p>
    </div>
  );
});

function App() {
  const [count, setCount] = useState(0);
  const user = { name: 'Алексей', email: 'alex@example.com' };

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Счёт: {count}
      </button>
      <UserCard user={user} />
    </div>
  );
}
// UserCard не перерендерится при изменении count

HOC vs Render Props vs Хуки

Характеристика HOC Render Props Хуки
Синтаксис Функция обёртки Проп-функция Стандартные функции
Сложность Средняя Средняя Низкая
Вложенность Можно цепочкой Проблема с «адским» рендером Отличная композиция
Гибкость Средняя Высокая Высокая
Современность Устаревает Устаревает Актуально
Когда использовать Старые проекты, библиотеки Старые проекты Новые проекты

Частые ошибки новичков

Ошибка Почему Исправление
Мутируют компонент в HOC Нарушает иммутабельность Создавай копию, а не мутируй
Не передают props Потеря данных Всегда передавай {...props}
Не используют displayName Сложно отлаживать Добавь displayName в HOC
Создают HOC внутри render Пересоздаётся при каждом рендере Создавай вне компонента
Слишком много HOC «Ад» из обёрток Комбинируй через compose

Задание для закрепления (на 1–2 часа)

Создай три HOC:

  1. withLoading — показывает индикатор загрузки (можно через проп isLoading или внутреннее состояние с таймером)
  2. withPermission — проверяет права доступа: принимает список разрешений (permissions) и проверяет их перед рендером
  3. withTheme — добавляет тему (светлая/тёмная) через контекст и передаёт её в компонент через пропс theme

Дополнительно: создай страницу, где используются все три HOC:

  1. Компонент AdminPanel обёрнут в withAuth
  2. Компонент UserList обёрнут в withData и withLoading
  3. Компонент Dashboard обёрнут в withTheme и withPermissions

Проверка: убедись, что каждый HOC добавляет свою функциональность без изменения исходных компонентов.

Что дальше?

В следующем уроке разберём Portals — механизм рендеринга компонентов за пределами родительского DOM-дерева. Ты узнаешь, как создавать модальные окна, всплывающие подсказки и другие UI-элементы, которые должны появляться поверх всего контента, нарушая обычную иерархию React.

Теги: