От автора: работа с управлением состоянием в приложениях React может быть непростой задачей, особенно когда данные необходимо передать от корневого компонента к глубоко вложенным компонентам.
Мы, как разработчики React, часто склонны перепроектировать свои приложения, слишком сильно полагаясь на Context API и Redux в ситуациях, когда они на самом деле не нужны. Мы неоправданно часто обращаемся к этим инструментам — даже в простейших ситуациях, когда просто требуется передача состояния/данных глубоко вложенным компонентам — и все это в попытке решить задачу сквозной передачи данных.
В некоторых случаях это совершенно нормально, но в других случаях это добавляет избыточности нашему приложению. Каждый компонент, который потребляет или использует эти провайдеры, перерисовывается всякий раз, когда происходит изменение состояния.
Очень немногие разработчики останавливаются, чтобы взглянуть на саму библиотеку React для решения некоторых проблем — или даже рассмотреть возможность лучшей альтернативы передаче данных вниз по дереву компонентов.
React сам по себе также является библиотекой управления состоянием, которая предоставляет собственное удобное решение для управления состоянием, особенно для такой вещи, как передача данных глубоко вложенным компонентам. Эта статья призвана предоставить вам четкое руководство о том, как это сделать, и продемонстрировать преимущества использования React вместо Context API или Redux.
Что такое проп дриллинг и почему это проблема?
Мы не можем искать решение проблемы, не взглянув сначала на саму проблему. Итак, что же такое проп дриллинг и почему это проблема?
Проп дриллинг (Prop drilling) — это неофициальный термин для передачи данных через несколько вложенных дочерних компонентов в попытке доставить их глубоко вложенному компоненту. Проблема с этим подходом заключается в том, что большинству компонентов, через которые передаются данные — они не нужны. Транзитные компоненты просто используются в качестве среды для транспортировки этих данных к компоненту назначения.
Именно здесь появляется термин «дриллинг», поскольку компоненты вынуждены принимать несвязанные данные и передавать их следующему компоненту, который, в свою очередь, передает их, и так далее, пока не достигнет места назначения. Это может вызвать серьезные проблемы для повторного использования компонентов и производительности приложения, о чем мы расскажем позже. А пока давайте рассмотрим пример, который может привести к проп дриллингу.
Создание глубоко вложенного приложения с проп дриллингом
Представьте на секунду, что мы создаем приложение, которое приветствует пользователя по имени при входе в систему. Ниже представлено визуальное представление демонстрационного приложения, которое мы будем рассматривать.
Мы не будем описывать стили, чтобы наш код был минимальным; эта схема просто для того, чтобы дать четкое представление о том, как будет выглядеть наше приложение. Теперь давайте посмотрим на иерархию компонентов, чтобы понять отношения между компонентами.
Как вы, наверное, видите, проблема заключается в том, что объект user, содержащий имя пользователя, доступен только на уровне корневого компонента (приложение), тогда как компонент, отображающий приветственное сообщение, вложен глубоко в наше приложение (сообщение). Это означает, что мы каким-то образом должны передать объект user компоненту, который отображает приветственное сообщение.
Синие стрелки представляют передачу props объекта user, от корневого компонента приложения через несколько вложенных компонентов к фактическому компоненту сообщения, который в них нуждается. Затем, наконец, отображается приветственное сообщение с именем вошедшего в систему пользователя.
Это типичный случай проп дриллинга. Именно здесь разработчики часто прибегают к Context API как к средству обхода предполагаемой проблемы, не задумываясь о потенциальных проблемах, возникающих после. Теперь, когда у нас есть визуальная карта проекта, давайте перейдем к реальному коду.
import { useState } from "react"; function App() { const [user, setUser] = useState({ name: "Steve" }); return ( <div> <Navbar /> <MainPage user={user} /> </div> ); } export default App; // Navbar Component function Navbar() { return <nav style={{ background: "#10ADDE", color: "#fff" }}>Demo App</nav>; } //MainPage Component function MainPage({ user }) { return ( <div> <h3>Main Page</h3> <Content user={user} /> </div> ); } // Content Component function Content({ user }) { return ( <div> <Message user={user} /> </div> ); } //Message Component function Message({ user }) { return <p>Welcome {user.name}</p>; }
Обратите внимание, что вместо того, чтобы разбивать наши компоненты на разные файлы, а затем импортировать каждый отдельный компонент, мы помещаем их все в один и тот же файл как отдельные функциональные компоненты. Мы можем использовать их без внешнего импорта. Наш результирующий вывод будет:
Теперь, когда у нас есть базовое рабочее приложение, давайте решим проблему проп дриллинга его еще раз, на этот раз с помощью Context API.
Решение проблемы проп дриллинга с помощью Context API
Для тех, кто не знаком с Context API, мы начнем с краткого обзора того, что он делает. Context API в основном позволяет вам транслировать ваше состояние/данные нескольким компонентам, обертывая их контекст-провайдером. Context API передает состояние контекст-провайдеру, используя атрибут value. Дочерние компоненты могут подключиться к этому провайдеру с помощью context consumer или хука useContext, когда это необходимо, и получить доступ к состоянию, предоставленному контекст-провайдером.
Давайте создадим контекст и передадим объект user контекст-провайдеру. Затем обернем нужные компоненты контекст-провайдером и получим доступ к состоянию, которое он содержит, внутри конкретного компонента.
import "./App.css"; import { createContext, useContext } from "react"; //Creating a context const userContext = createContext(); function App() { return ( <div> <Navbar /> <userContext.Provider value={{ user: "Steve" }}> <MainPage /> </userContext.Provider> </div> ); } export default App; function Navbar() { return <nav style={{ background: "#10ADDE", color: "#fff" }}>Demo App</nav>; } function MainPage() { return ( <div> <h3>Main Page</h3> <Content /> </div> ); } function Content() { return ( <div> <Message /> </div> ); } function Message() { // Getting access to the state provided by the context provider wrapper const { user } = useContext(userContext); return <p>Welcome {user} :)</p>; }
Мы начинаем с импорта хука createContext, который используется для создания контекста, и хука useContext, который будет извлекать состояние, предоставленное контекст-провайдером.
Затем мы вызываем хук createContext, который возвращает объект контекста с пустым значением. Этот объект сохраняется в переменной с именем userContext.
Оборачиваем компонент MainPage с помощью Context.Provider и передаем ему объект user, который предоставляет его каждому компоненту, вложенному в компонент MainPage.
Наконец, мы извлекаем user из компонента Message, вложенного в компонент MainPage, используя хук useContext и небольшую деструктуризацию.
Мы полностью устранили необходимость передачи user prop через промежуточные компоненты. В результате мы решили вопрос проп дриллинга.
Наш результат остается прежним, но код стал немного компактнее и чище. Итак, почему это проблема?
Два основных недостатка сильной зависимости от Context API
Хотя мы полностью решили проблему проп дриллинга, внедрив в наше приложение Context API, здесь не обошлось без собственных предостережений, таких как проблемы с повторным использованием компонентов и производительностью.
Эти предостережения, хотя и незначительные в небольших приложениях, могут в равной степени привести к нежелательным результатам. Документация Context API предупреждают об этих предостережениях:
Прежде чем использовать контекст:
Контекст в основном используется, когда определенные данные должны быть доступны многим компонентам на разных уровнях вложенности. Применяйте его с осторожностью, потому что он затрудняет повторное использование компонентов.
Если вы хотите избежать передачи props через множество уровней, композиция компонентов часто является более простым решением, чем контекст.
Проблемы с возможностью повторного использования компонентов
Когда контекст-провайдер охватывает несколько компонентов, мы неявно передаем любое состояние или данные, которые хранятся в этом провайдере, дочерним компонентам, которые он обертывает.
Заметьте, я сказал «неявно». Мы не передаем состояние этим компонентам буквально — до тех пор, пока не инициируем фактического context consumer или пока не используем хук useContext — но мы неявно сделали эти компоненты зависимыми от состояния, предоставляемого контекст-провайдером.
Проблема связана с попыткой повторного использования любого из этих компонентов за пределами нашего контекст-провайдера. Компонент сначала пытается подтвердить, существует ли еще то неявное состояние, предоставленное контекст-провайдером, до рендеринга. Когда он не находит этого состояния, он выдает ошибку рендеринга.
Все еще не ясно? Представьте на секунду наш предыдущий пример. Допустим, мы хотели повторно использовать компонент Message для отображения другого сообщения на основе другого условия, и этот компонент Message должен был быть размещен за пределами враппера контекст-провайдера.
import { createContext, useContext } from "react"; //Creating a context const userContext = createContext(); function App() { return ( <> <div> <Navbar /> <userContext.Provider value={{ user: "Steve" }}> <MainPage /> </userContext.Provider> </div> {/* Trying to use the message component outside the Context Provider*/} <Message /> </> ); } export default App; function Navbar() { return <nav style={{ background: "#10ADDE", color: "#fff" }}>Demo App</nav>; } function MainPage() { return ( <div> <h3>Main Page</h3> <Content /> </div> ); } function Content() { return ( <div> <Message /> </div> ); } function Message() { // Getting access to the state provided by the context provider wrapper const { user } = useContext(userContext); return <p>Welcome {user} :)</p>; }
Результатом будет следующее:
Как видно выше, любая попытка сделать такое приведет к ошибке рендеринга, потому что компонент Message зависит от объекта user в состоянии контекст-провайдера. Попытки получить доступ к любому существующему объекту user, предоставленному контекст-провайдером, потерпят неудачу. Ниже приведена наглядная иллюстрация приведенного выше фрагмента.
Некоторые разработчики предлагают обойти проблему, обернув контекстом все приложение. Это подошло бы для небольших приложений, но с более крупными или более сложными приложениями такое решение может быть непрактичным, поскольку мы часто хотим охватить несколько контекст-провайдеров в приложении, в зависимости от того, чем нужно управлять.
Проблемы с производительностью
Context API использует алгоритм сравнения, который сравнивает значение своего текущего состояния с любым обновлением, которое он получает, и всякий раз, когда происходит изменение, Context API передает это изменение каждому компоненту, использующему его провайдера, что, в свою очередь, приводит к повторному рендерингу этих компонентов.
На первый взгляд это может показаться тривиальным, но когда мы в значительной степени полагаемся на Context для базового управления состоянием, мы перепроектируем наше приложение, напрасно передавая все состояния контекст-провайдеру. Как и следовало ожидать, это не очень эффективно, когда многие компоненты зависят от этого контекст-провайдера, поскольку они будут повторно отображаться всякий раз, когда происходит обновление состояния, независимо от того, касается ли изменение этих компонентов или нет.
Знакомство с композицией компонентов
Вспомним несколько советов от создателей React:
Если вы хотите избежать передачи нескольких props через множество уровней, композиция компонентов часто является более простым решением, чем контекст.
Вы можете узнать эту цитату из документации React, на которую я ссылался ранее — если быть точным, она находится в разделе Context API.
Новые разработчики React могут задаться вопросом, что означает «композиция компонентов». Композиция компонентов — это не новая функция, осмелюсь сказать, что это фундаментальный принцип, лежащий в основе React и многих фреймворков JavaScript.
Когда мы создаем приложения React, мы делаем композицию, создавая несколько многократно используемых компонентов, которые можно рассматривать почти как независимые блоки Lego. Каждый блок (компонент) Lego затем считается частью нашего окончательного интерфейса, которые, будучи собраны вместе, образуют законченный интерфейс нашего приложения.
Именно этот процесс сборки компонентов в виде блоков Lego известен как композиция компонентов.
Если вы уже создавали приложение React раньше (а я уверен, что так и было), вы, вероятно, использовали композицию компонентов, не осознавая, что это такое: альтернатива для управления состоянием нашего приложения. В этой статье мы сосредоточимся в основном на двух типах композиций компонентов: контейнерах компонентов и специализированных компонентах.
Контейнер компонентов
Как и все в JavaScript (кроме примитивных типов данных), компоненты в React — это не что иное, как объекты, и, как обычные объекты, компоненты могут содержать различные разновидности свойств, включая и другие компоненты. Есть два способа достичь этого:
Путем явной передачи одного или нескольких компонентов другому компоненту в качестве prop этого компонента.
Обернув родительский компонент вокруг одного или нескольких дочерних компонентов, а затем перехватив эти дочерние компоненты, используя дочерное свойство prop.
Рассмотрим первый способ:
import {useState} from 'react' function App() { const [data, setData] = useState("some state"); return <ComponentOne ComponentTwo={<ComponentTwo data={data} />} />; } function ComponentOne({ ComponentTwo }) { return ( <div> <p>This is Component1, it receives component2 as a prop and renders it</p> {ComponentTwo} </div> ); } function ComponentTwo({ data }) { return <h3>This is Component two with the received state {data}</h3>; }
Вместо того, чтобы вкладывать компоненты в компоненты, а затем пытаться передать им данные с помощью проп дриллинга, мы можем просто перенести эти компоненты в наше корневое приложение, а затем вручную передать дочерние компоненты родительскому с необходимыми данными. Затем родительский компонент отобразит их как prop.
Теперь рассмотрим второй способ:
function App() { const [data, setData] = useState("some state"); return ( <ParentComponent> <ComponentOne> <ComponentTwo data={data} /> </ComponentOne> </ParentComponent> ); } function ParentComponent({ children }) { return <div>{children}</div>; } function ComponentOne({ children }) { return ( <> <p>This is Component1, it receives component2 as a child and renders it</p> {children} </> ); } function ComponentTwo({ data }) { return <h3>This is Component two with the received {data}</h3>; }
На этом этапе код должен говорить сам за себя — всякий раз, когда мы оборачиваем компонент вокруг другого, компонент-оболочка становится родительским компонентом для обернутого. Затем дочерний компонент может быть получен в родительском компоненте с использованием дочернего prop по умолчанию, которая отвечает за рендеринг дочерних компонентов.
Специализированные компоненты
Специализированный компонент — это универсальный компонент, созданный для рендеринга специализированных вариантов самого себя путем передачи props, соответствующих условиям для конкретного варианта.
Эта форма компоновки компонентов не обязательно решает задачу проп дриллинга, но больше связана с возможностью повторного использования и созданием меньшего количества компонентов, которые могут эффективно играть ключевую роль в создании интерфейса с отслеживанием состояния в сочетании с компонентами-контейнерами.
Ниже приведен пример специализированного компонента и то, как он облегчает повторное использование.
function App() { return ( <PopupModal title="Welcome" message="A popup modal"> <UniqueContent/> </PopupModal> ); } function PopupModal({title, message, children}) { return ( <div> <h1 className="title">{title}</h1> <p className="message">{message}</p> {children && children} </div> ); } function UniqueContent() { return<div>Unique Markup</div> }
Почему композиция компонентов — это важно
Теперь, когда вы немного разбираетесь в составе компонентов, не должно быть ничего сложного в том, чтобы выяснить, насколько полезным может быть композиция компонентов. Перечислим несколько причин:
Она способствует повторному использованию компонентов
Она легко решает предполагаемую проблему проп дриллинга без внешних библиотек.
Поднимая большинство наших компонентов на корневой уровень и разумно комбинируя различные методы композиции, она может стать эффективной альтернативой для управления состоянием.
Композиция делает ваш код более предсказуемым и упрощает отладку
Она легко расширяет возможности обмена состоянием и функциями с другими компонентами.
По сути, это способ React для создания интерфейсов.
Я мог бы продолжить рассказ о различных аспектах важности композиции компонентов, но вы уже должны увидеть закономерность.
Воссоздание нашего приложения с использованием композиции компонентов
Давайте реорганизуем наше приложение, чтобы использовать композицию компонентов. Мы сделаем это двумя способами, чтобы продемонстрировать ее гибкость.
import { useState } from "react"; function App() { const [user, setState] = useState({ name: "Steve" }); return ( <div> <Navbar /> <MainPage content={<Content message={<Message user={user} />} />} /> </div> ); } export default App; function Navbar() { return <nav style={{ background: "#10ADDE", color: "#fff" }}>Demo App</nav>; } function MainPage({ content }) { return ( <div> <h3>Main Page</h3> {content} </div> ); } function Content({ message }) { return <div>{message}</div>; } function Message({ user }) { return <p>Welcome {user.name} :)</p>; }
Или:
function App() { const [user, setState] = useState({ name: "Steve" }); return ( <div> <Navbar /> <MainPage> <Content> <Message user={user} /> </Content> </MainPage> </div> ); } export default App; function Navbar() { return <nav style={{ background: "#10ADDE", color: "#fff" }}>Demo App</nav>; } function MainPage({ children }) { return ( <div> <h3>Main Page</h3> {children} </div> ); } function Content({ children }) { return <div>{children}</div>; } function Message({ user }) { return <p>Welcome {user.name} :)</p>; }
Как видно из обоих приведенных выше фрагментов, существует несколько способов композиции компонентов. В первом фрагменте мы воспользовались React props, чтобы передать компонент каждому родительскому компоненту в виде простого объекта с данными.
Во втором фрагменте мы воспользовались преимуществом свойства children, чтобы создать чистую композицию нашего макета с данными, напрямую переданными интересующему компоненту. Мы могли бы легко придумать другие способы рефакторинга этого приложения, используя только композицию компонентов, но к настоящему моменту вы должны ясно видеть возможности решения проблемы проп дриллинга, полагаясь только на композицию компонентов.
Заключение
React предоставляет мощную композицию для управления не только компонентами, но и состоянием в приложении. Как написано в документах React Context:
Контекст предназначен для обмена данными, которые можно считать «глобальными» для дерева компонентов React, таких как текущий аутентифицированный пользователь, тема или предпочитаемый язык.
Разработчики часто советуют меньше полагаться на Context или другие библиотеки для управления локальным состоянием, особенно если это делается для того, чтобы избежать проп дриллинга, и композиция компонентов — лучший выбор.
Автор: David Herbert
Источник: blog.logrocket.com
Редакция: Команда webformyself.
Читайте нас в Telegram, VK, Яндекс.Дзен