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

Render Props — когда компонент решает, что рендерить

Цель урока: понять паттерн Render Props — способ передачи функции в качестве пропса, которая определяет, что будет рендериться внутри компонента. Научиться создавать гибкие, переиспользуемые компоненты, которые разделяют логику, но не диктуют отображение.

Render Props — когда компонент решает, что рендерить

Введение: что такое Render Props

Render Props — это паттерн, при котором компонент принимает функцию в качестве пропса и вызывает её, возвращая JSX для рендера.

Главная мысль: Render Props — это способ сказать компоненту: «Вот тебе логика, а вот функция, которая решает, как это отобразить».

Почему это важно:

  1. Разделение логики и отображения
  2. Максимальная гибкость для пользователя компонента
  3. Альтернатива HOC (Higher Order Components)
  4. Особенно полезно для UI-библиотек

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

Компонент MouseTracker с Render Props:

import { useState, useEffect } from 'react';

// Компонент, который следит за позицией мыши
function MouseTracker({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMouseMove = (event) => {
      setPosition({
        x: event.clientX,
        y: event.clientY
      });
    };

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

  // Вызываем функцию render и передаём ей данные
  return render(position);
}

// Использование
function App() {
  return (
    <div>
      <h1>Позиция мыши</h1>
      <MouseTracker 
        render={({ x, y }) => (
          <div>
            <p>X: {x}, Y: {y}</p>
            <div 
              style={{
                width: '20px',
                height: '20px',
                background: 'red',
                position: 'absolute',
                left: x - 10,
                top: y - 10,
                borderRadius: '50%'
              }}
            />
          </div>
        )}
      />
    </div>
  );
}

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

  1. MouseTracker содержит логику отслеживания мыши
  2. Пользователь передаёт функцию через пропс render
  3. MouseTracker вызывает эту функцию с данными
  4. Функция возвращает JSX, который будет отрендерен

Альтернативное название — children как функция

Вместо пропса render часто используют children:

function MouseTracker({ children }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMouseMove = (event) => {
      setPosition({
        x: event.clientX,
        y: event.clientY
      });
    };

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

  return children(position);
}

// Использование
function App() {
  return (
    <MouseTracker>
      {({ x, y }) => (
        <p>Позиция: {x}, {y}</p>
      )}
    </MouseTracker>
  );
}

Что лучше: render или children?

  1. children — более идиоматично для React
  2. render — явно указывает на назначение пропса
  3. В современных проектах чаще используют children

Реальный пример: компонент с загрузкой данных

// Компонент, который загружает данные и передаёт их через render prop
function DataFetcher({ url, children }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        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]);

  return children({ data, loading, error });
}

// Использование — загрузка пользователей
function UsersPage() {
  return (
    <DataFetcher url="https://api.example.com/users">
      {({ data, loading, error }) => {
        if (loading) return <p>Загрузка пользователей...</p>;
        if (error) return <p>Ошибка: {error}</p>;
        if (!data) return <p>Нет данных</p>;

        return (
          <ul>
            {data.map(user => (
              <li key={user.id}>{user.name}</li>
            ))}
          </ul>
        );
      }}
    </DataFetcher>
  );
}

// То же самое, но для продуктов
function ProductsPage() {
  return (
    <DataFetcher url="https://api.example.com/products">
      {({ data, loading, error }) => {
        if (loading) return <p>Загрузка товаров...</p>;
        if (error) return <p>Ошибка: {error}</p>;
        if (!data) return <p>Нет товаров</p>;

        return (
          <div>
            {data.map(product => (
              <div key={product.id}>
                <h3>{product.name}</h3>
                <p>Цена: {product.price}₽</p>
              </div>
            ))}
          </div>
        );
      }}
    </DataFetcher>
  );
}

Преимущество: Один компонент — разное отображение!

Из ноутбука появляются летающие светящиеся экраны

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

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

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

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

Render Props с несколькими пропсами

function WindowSize({ children }) {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return children({
    width: size.width,
    height: size.height,
    isMobile: size.width < 768,
    isTablet: size.width >= 768 && size.width < 1024,
    isDesktop: size.width >= 1024
  });
}

// Использование
function ResponsiveLayout() {
  return (
    <WindowSize>
      {({ width, height, isMobile, isDesktop }) => (
        <div>
          <p>Ширина: {width}px</p>
          <p>Высота: {height}px</p>
          
          {isMobile && <MobileMenu />}
          {isDesktop && <DesktopMenu />}
          
          <p>Тип устройства: {
            isMobile ? 'Мобильное' : 
            isDesktop ? 'Десктоп' : 'Планшет'
          }</p>
        </div>
      )}
    </WindowSize>
  );
}

Render Props с параметрами

Можно передавать параметры внутрь render-функции:

function Toggle({ children }) {
  const [isOn, setIsOn] = useState(false);

  const toggle = () => setIsOn(prev => !prev);
  const setOn = () => setIsOn(true);
  const setOff = () => setIsOn(false);

  return children({
    isOn,
    toggle,
    setOn,
    setOff,
    // Вспомогательные функции
    getButtonProps: () => ({
      onClick: toggle,
      'aria-pressed': isOn
    })
  });
}

// Использование
function ThemeToggle() {
  return (
    <Toggle>
      {({ isOn, toggle, getButtonProps }) => (
        <div>
          <button {...getButtonProps()}>
            {isOn ? '🌙 Тёмная' : '☀️ Светлая'}
          </button>
          
          <div style={{
            background: isOn ? '#333' : '#fff',
            color: isOn ? '#fff' : '#333',
            padding: '20px',
            marginTop: '10px'
          }}>
            Тема: {isOn ? 'Тёмная' : 'Светлая'}
          </div>
        </div>
      )}
    </Toggle>
  );
}

Композиция Render Props

Можно комбинировать несколько компонентов с render props:

function App() {
  return (
    <WindowSize>
      {({ isMobile }) => (
        <DataFetcher url={isMobile ? '/api/mobile' : '/api/desktop'}>
          {({ data, loading }) => (
            <MouseTracker>
              {({ x, y }) => (
                <div>
                  <p>Размер: {isMobile ? 'Мобильный' : 'Десктоп'}</p>
                  <p>Данные: {loading ? 'Загрузка...' : data.length}</p>
                  <p>Мышь: {x}, {y}</p>
                </div>
              )}
            </MouseTracker>
          )}
        </DataFetcher>
      )}
    </WindowSize>
  );
}

Важно: Слишком глубокая вложенность может ухудшить читаемость.

Render Props против HOC (Higher Order Components)

Характеристика Render Props HOC
Синтаксис Проп-функция Функция, оборачивающая компонент
Передача данных Через аргументы функции Через пропсы
Гибкость Высокая (можно выбирать, что рендерить) Средняя
Вложенность Может быть проблема с «адским» рендером Можно цепочкой
Простота Интуитивно понятно Требует понимания HOC-концепции
Когда использовать Когда нужно контролировать рендер Когда нужно обогатить компонент пропсами

Современная альтернатива — хуки

В современном React Render Props часто заменяются хуками:

// Хук вместо Render Props
function useMouseTracker() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMouseMove = (event) => {
      setPosition({
        x: event.clientX,
        y: event.clientY
      });
    };

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

  return position;
}

// Использование
function App() {
  const { x, y } = useMouseTracker();

  return (
    <div>
      <p>X: {x}, Y: {y}</p>
      <div style={{
        width: '20px',
        height: '20px',
        background: 'red',
        position: 'absolute',
        left: x - 10,
        top: y - 10,
        borderRadius: '50%'
      }} />
    </div>
  );
}

Когда использовать Render Props vs Хуки:

  1. Хуки — в новых проектах (проще и чище)
  2. Render Props — в старых проектах или при создании UI-библиотек
  3. Render Props — когда нужно передавать данные не только компоненту, но и его потомкам

Компонент с несколькими render-пропсами

function ComplexLayout({ 
  header, 
  sidebar, 
  content, 
  footer 
}) {
  const [theme, setTheme] = useState('light');
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div className="layout">
      <div className="header">
        {header({ theme, toggleTheme: () => setTheme(prev => prev === 'light' ? 'dark' : 'light') })}
      </div>
      
      <div className="body">
        <div className="sidebar">
          {sidebar({ isOpen, toggle: () => setIsOpen(prev => !prev) })}
        </div>
        
        <div className="content">
          {content({ theme })}
        </div>
      </div>
      
      <div className="footer">
        {footer({ theme })}
      </div>
    </div>
  );
}

// Использование
function App() {
  return (
    <ComplexLayout
      header={({ theme, toggleTheme }) => (
        <header>
          <h1>Мой сайт</h1>
          <button onClick={toggleTheme}>
            {theme === 'light' ? '🌙' : '☀️'}
          </button>
        </header>
      )}
      
      sidebar={({ isOpen, toggle }) => (
        <nav>
          <button onClick={toggle}>☰ Меню</button>
          {isOpen && (
            <ul>
              <li>Главная</li>
              <li>О нас</li>
              <li>Контакты</li>
            </ul>
          )}
        </nav>
      )}
      
      content={({ theme }) => (
        <main>
          <h2>Основной контент</h2>
          <p>Тема: {theme}</p>
        </main>
      )}
      
      footer={({ theme }) => (
        <footer>
          <p>© 2025 Все права защищены</p>
          <p>Тема: {theme}</p>
        </footer>
      )}
    />
  );
}

Render Props в React Router (пример)

В React Router до версии 6 использовались render props:

import { BrowserRouter, Route } from 'react-router-dom';

<Route
  path="/users/:id"
  render={({ match }) => {
    const userId = match.params.id;
    return <UserProfile userId={userId} />;
  }}
/>

Сейчас используется element пропс:

<Route path="/users/:id" element={<UserProfile />} />

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

Ошибка Почему Исправление
Не вызывают функцию рендера Ничего не отображается Всегда вызывай children(data) или render(data)
Создают функцию внутри render Лишние перерендеры Мемоизируй через useCallback
Забывают про PropTypes Нет валидации Добавь PropTypes для render-функции
Передают функцию не в тот пропс Не работает Проверь имя пропса (render или children)
Вложенные render props «Ад» из вложений Выноси в отдельные компоненты

Итог (что мы узнали)

Что узнали Как это работает
Render Props Компонент с функцией для рендера
children как функция Альтернатива render пропсу
Передача данных Через аргументы функции
Композиция Комбинация нескольких render-пропсов
Против HOC Разные подходы к общему коду
Против хуков Хуки — современная альтернатива

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

Создай три компонента с Render Props:

List — компонент, который рендерит список элементов:

  1. Принимает items (массив) и renderItem (функция, принимающая элемент и индекс)
  2. Добавляет разделители между элементами (если передан пропс withSeparator)

Pagination — компонент для пагинации:

  1. Принимает totalItems (всего), pageSize (на странице)
  2. Передаёт в render-функцию: currentPage, totalPages, goToPage, nextPage, prevPage

Form — компонент управления формой:

  1. Принимает initialValues и onSubmit
  2. Передаёт в render-функцию: values, handleChange, handleSubmit, errors

Дополнительно: Создай страницу, которая использует все три компонента вместе — список товаров с пагинацией и формой поиска (поиск управляется через Form).

Проверка: Убедись, что все компоненты работают гибко — пользователь сам решает, как отображать данные.

Что дальше?

В следующем уроке разберём High Order Components (HOC) — продвинутый паттерн для обогащения компонентов дополнительной логикой. Ты узнаешь, как создавать функции-обёртки, которые добавляют функциональность (авторизация, логирование, загрузка) без изменения самого компонента.

Теги: