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

—

от автора

Предисловие

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

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

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

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

Например:

const ParentComponent = () => {   return (     <ChildComponent       render={(text) => <h1>{text}</h1>}     />   ); };  const ChildComponent = ({ render }) => {   const text = "Hello World";   return <div>{render(text)}</div>; };      // Получаем такой код <div>   <h1>Hello World</h1> </div>      // В качестве названия рендер-пропса можно использовать // любой текст. "render" в примере используется исключительно  // в целях удобства понимания.

На 4 строке видно, что переданный text из ChildComponent мы отрисовываем внутри <h1> тега, но это всего лишь наименьшая обёртка, сделанная для простоты примера. Мы можем манипулировать получаемыми данными как-угодно!

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

const ParentComponent = () => {   return (     <ChildComponent       render={(text) => {         return (           <div style={{color: "#7d7d7d"}}>             <h1>{text}</h1>             <div>Какое-то описание...</div>           </div>         )       }}     />   ); };  const ChildComponent = ({ render }) => {   const text = "Hello World";   return <div>{render(text)}</div>; };    // Получаем такой код <div>   <h1 style="color: #7d7d7d;">Hello World</h1>   <div>Какое-то описание...</div> </div>

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

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

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

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

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

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

const ClickCounter = ({ render }) => {   const [count, setCount] = useState(0);    const increment = () => setCount(count + 1);    return <div>{render({ count, increment })}</div> };

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

<ClickCounter   render={({ count, increment }) => (     <div>       <h2>Кастомный счётчик</h2>       <p>Количество кликов: {count}</p>       <button onClick={increment}>Прибавить 1</button>     </div>   )} />

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

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

1. <Form />

Рассмотрим вот такой компонент для отправки введённых значений. Он хранит в себе функции 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 (   <form onSubmit={handleSubmit}>     {Object.keys(initialValues).map((key) => (       <div key={key}>         <label>           <div>{key[0].toUpperCase() + key.slice(1)}:</div>           <input             type="text"             name={key}             value={values[key]}             onChange={handleChange}           />         </label>       </div>     ))}     <button type="submit">Отправить</button>   </form>   ); };

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

<Form initialValues={{ username: "", email: "" }} />

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

<Form   initialValues={{ username: "", email: "" }}   render={({ values, handleChange, handleSubmit }) => (     <form onSubmit={handleSubmit}>       <h2>Кастомная форма</h2>              <div>         <label>           <div>Пользователь:</div>           <input             type="text"             name="username"             value={values.username}             onChange={handleChange}           />         </label>       </div>      <div>       <label>         <div>Электронная почта:</div>         <input           type="email"           name="email"           value={values.email}           onChange={handleChange}         />       </label>     </div>      <button type="submit">Отправить</button>   </form>   )} />

2. <Pagination />

Компонент пагинации по способу определения компонента аналогичен 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 (     <div>       <p>         Страница {currentPage} из {totalPages}       </p>       <button         onClick={() => goToPage(currentPage - 1)}         disabled={currentPage === 1}       >         Назад       </button>       <button         onClick={() => goToPage(currentPage + 1)}         disabled={currentPage === totalPages}       >         Вперёд       </button>     </div>   ); };

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

<Pagination totalItems={100} itemsPerPage={10} />

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

<Pagination   totalItems={100}   itemsPerPage={10}   render={({ currentPage, totalPages, goToPage }) => (     <div>       <h2>Кастомная пагинация</h2>       <button onClick={() => goToPage(1)} disabled={currentPage === 1}>         Первая       </button>       <button         onClick={() => goToPage(currentPage - 1)}         disabled={currentPage === 1}       >         Назад       </button>       <span>         Страница {currentPage} из {totalPages}       </span>       <button         onClick={() => goToPage(currentPage + 1)}         disabled={currentPage === totalPages}       >         Вперёд       </button>       <button         onClick={() => goToPage(totalPages)}         disabled={currentPage === totalPages}       >         Последняя       </button>     </div>   )} />

3. <CopyToClipboard />

Компонент 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 (     <div>       <p>Текст для копирования: {text}</p>       <button onClick={handleCopy}>         {copied ? "Скопировано!" : "Скопировать"}       </button>     </div>   ); }

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

<CopyToClipboard text="https://example.com" />

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

<CopyToClipboard   text="https://example.com"   render={({ copied, handleCopy }) => (     <div style={{ display: "flex", alignItems: "center", gap: "10px" }}>       <input         type="text"         value="https://example.com"         readOnly         style={{ padding: "5px", width: "300px" }}       />       <button onClick={handleCopy}>         {copied ? "Скопировано!" : "Копировать"}       </button>     </div>   )} />

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 (     <div>       <h2>Кастомный счётчик</h2>       <p>Количество кликов: {count}</p>       <button onClick={increment}>Прибавить 1</button>     </div>   ); };

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

Плюсы Render props:

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

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

Минусы Render props:

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

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

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

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

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

Итог

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


ссылка на оригинал статьи https://habr.com/ru/articles/876758/