Зачем нужен шаблон Render props в React?

38959d26326fea67584d66887626122e.png

Предисловие

Не часто приходится встречать людей, которые понимают зачем он нужен и ещё реже попадаются проекты, в которых его используют. А шаблон-то очень полезный!

В этой статье вы поймёте как он устроен, зачем он нужен и на примерах научитесь его правильно применять.

Как он устроен?

Главная смысл шаблона — передача в качестве props функции, которая будет принимать какие-то данные от дочернего компонента и отрисовывать их так, как будет указано в родительском.

Например:

const ParentComponent = () => {
  return (
     

{text}

} /> ); }; const ChildComponent = ({ render }) => { const text = "Hello World"; return
{render(text)}
; }; // Получаем такой код

Hello World

// В качестве названия рендер-пропса можно использовать // любой текст. "render" в примере используется исключительно // в целях удобства понимания.

На 4 строке видно, что переданный text из ChildComponent мы отрисовываем внутри

тега, но это всего лишь наименьшая обёртка, сделанная для простоты примера. Мы можем манипулировать получаемыми данными как-угодно!

Например, добавить какой-то статический текст или стили:

const ParentComponent = () => {
  return (
     {
        return (
          

{text}

Какое-то описание...
) }} /> ); }; const ChildComponent = ({ render }) => { const text = "Hello World"; return
{render(text)}
; }; // Получаем такой код

Hello World

Какое-то описание...

Зачем он нужен?

Как видно из примера выше, данный шаблон даёт нам больше гибкости в том, как мы можем отобразить содержимое нужного нам компонента.

Это может быть особенно удобным в тех случаях, когда у нас есть компонент с каким-то определённым UI и какой-то определённой «логикой» внутри, но на отдельных страницах его UI должен быть чуточку другим, а механизм работы должен остаться тем же.

Гипотетический пример

Для разминки сначала разберём простой гипотетический пример со счётчиком кликов.

У нас есть базовая «логика» в виде count, setCount и increment. И мы сразу прокидываем эту логику наружу к внешнему компоненту при помощи функции render:

const ClickCounter = ({ render }) => {
  const [count, setCount] = useState(0);

  const increment = () => setCount(count + 1);

  return 
{render({ count, increment })}
};

Во внешнем компоненте мы эти данные получаем и отрисовываем любым удобным для нас образом:

 (
    

Кастомный счётчик

Количество кликов: {count}

)} />

Реальные примеры

До этого мы рассматривали примеры только с обязательным пропсом render. Но мы также можем сделать его необязательным и отрисовывать какой-то UI по-умолчанию в том случае, если он не был передан. В следующих трёх примерах мы как раз рассмотрим этот подход.

1.

Рассмотрим вот такой компонент для отправки введённых значений. Он хранит в себе функции handleChange и handleSubmit для обработки данных, а также UI, который рендерится самостоятельно в том случае, если функция render не была передана внутрь, иначе данные пробрасываются наружу и могут быть отрендерены как-угодно компонентом выше.

const Form = ({ initialValues, render }) => {
  const [values, setValues] = useState(initialValues);

  const handleChange = (event) => {
    const { name, value } = event.target;
    setValues((previousValues) => ({ ...previousValues, [name]: value }));
  };

  const handleSubmit = (event) => {
    event.preventDefault();
    console.log("Отправленные значения", values);
  };

  if (render) {
    return render({
      values,
      handleChange,
      handleSubmit,
    });
  } 
    
return (
  
    {Object.keys(initialValues).map((key) => (
      
))} ); };

Чтобы получить UI, который компонент предоставляет по-умолчанию, мы можем воспользоваться вот такой конструкцией:

Если нам понадобится кастомный UI, то мы можем воспользоваться пропсом render:

 (
    
      

Кастомная форма

)} />

2.

Компонент пагинации по способу определения компонента аналогичен Form, но содержит другую «логику»:

const Pagination = ({ totalItems, itemsPerPage, render }) => {
  const [currentPage, setCurrentPage] = useState(1);
  const totalPages = Math.ceil(totalItems / itemsPerPage);

  const goToPage = (page) => {
    if (page >= 1 && page <= totalPages) {
      setCurrentPage(page);
    }
  };

  if (render) {
    return render({ currentPage, totalPages, goToPage });
  }
    
  return (
    

Страница {currentPage} из {totalPages}

); };

Определение компонента с UI, предоставляемым по-умолчанию:

Определение компонента с кастомным UI:

 (
    

Кастомная пагинация

Страница {currentPage} из {totalPages}
)} />

3.

Компонент CopyToClipboard также аналогичен предыдущим двум по способу определения компонента, но содержит другую «логику» внутри:

const CopyToClipboard = ({ text, render }) => {
  const [copied, setCopied] = useState(false);

  const handleCopy = async () => {
    try {
      await navigator.clipboard.writeText(text);
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    } catch (error) {
      console.error("Ошибка копирования текста:", error);
    }
  };

  if (render) {
    return render({ copied, handleCopy });
  }

  return (
    

Текст для копирования: {text}

); }

Определение компонента с UI, предоставляемым по умолчанию:

Определение компонента с кастомным UI:

 (
    
)} />

Render props VS Пользовательские хуки

Опытный разработчик скорее всего заметит, что данный подход очень похож на подход с использованием пользовательских хуков. И, да, так и есть, эти подходы имеют очень много общего.

Так, ClickCounter из примера выше, можно было бы переделать таким образом:

const useClickCounter = () => {
  const [count, setCount] = useState(0);

  const increment = () => setCount(count + 1);

  return { count, increment };
};

И использовать вот так:

const SomeComponent = () => {
  const { count, increment } = useClickCounter();

  return (
    

Кастомный счётчик

Количество кликов: {count}

); };

Но всё же, подходы не равны на 100% и у каждого есть как свои преимущества, так и недостатки.

Плюсы Render props:

  1. Возможность неограниченного переиспользования логики компонента с другим UI без надобности создания клона компонента.

  2. Компонент не перегружается теми UI, которые ему не нужны и используются только в единичных случаях.

Минусы Render props:

  1. При использовании сложных или вложенных друг в друга Render props ухудшается читабельность кода.

Плюсы пользовательских хуков:

  1. Возможность переиспользования «логики» между разными компонентами.

Плюсы пользовательских хуков:

  1. В случае, когда на странице необходимо отрендерить большое количество однотипных элементов, но с небольшими различиями в UI, придётся для каждого создать отдельный компонент или под каждый такой блок кода сверху родительского компонента объявить пользовательский хук, что является нежелательным, так как каждое изменение состояния даже в одном из множества таких хуков будет вызывать перерендеринг всего компонента.

Итог

Как вы могли заметить из примеров выше, шаблон Render props — это очень полезная фича! Иногда её действительно можно использовать вместо пользовательских хуков, а иногда можно комбинировать вместе.

© Habrahabr.ru