Pull to refresh
831.71
OTUS
Цифровые навыки от ведущих экспертов

React-компоненты шаблонов проектирования

Reading time18 min
Views11K
Original author: Alexi Taylor

Введение

Эта документация поможет найти компромиссы между различными шаблонами (patterns) React, а также определить, когда использование каждого из них будет наиболее целесообразным. Нижеприведенные шаблоны позволят получить более практичный и многократно используемый код, придерживаясь принципов проектирования, таких как разделение ответственности, DRY (Don’t repeat yourself - не повторяй себя) и повторное использование кода. Некоторые из этих шаблонов помогут решить проблемы, которые возникают в больших React приложениях, таких как пробрасывание (prop drilling) или управление состоянием. Каждый основной шаблон включает пример, размещенный на CodeSandBox.

Приведенные ниже примеры не являются сложными, чтобы не смущать читателя деталями имплементации, которые не относятся к концепциям каждого шаблона компонентов.

Обзор

Составные компоненты — это шаблон, в котором компоненты используются вместе так, что они имеют неявное состояние (implicit state), позволяющее им общаться друг с другом в фоновом режиме. Составной компонент состоит из подмножества дочерних компонентов, которые все работают в тандеме для получения определенной функциональности.

Подумайте о составных компонентах, таких как элементы <select> и <option> в HTML. Порознь, они не слишком много делают, но вместе они позволяют создать полноценный результат. (Кент С. Доддс)

Зачем использовать составные компоненты? Какую ценность они обеспечивают?

Как создатель компонента многократного использования, вы должны помнить о его потребителе: других инженерах, которые будут использовать ваш компонент. Такая модель обеспечивает гибкость для пользователей компонентов. Она позволяет вам абстрагироваться от внутренней работы ваших компонентов; логика, лежащая в основе вашего многократно используемого компонента, не должна заботить потребителей. Она обеспечивает удобный и понятный интерфейс, в котором пользователь компонента заботится только о распределении совмещаемых элементов, обеспечивая при этом целостное восприятие.

Пример

Окунемся в пример и создадим форму радиоизображения. Мы создадим форму радиогруппы, но вместо того, чтобы показывать обычные радиокнопки, мы отобразим список изображений, из которых пользователь может выбирать. Вы можете проследить за конечным результатом в CodeSandBox.

Мы создадим один родительский компонент (parent component), RadioImageForm, который будет отвечать за логику формы, и один дочерний, "подкомпонент", RadioInput, который будет отображать изображения радиовходов. Вместе они создадут один составной компонент.

{/* The parent component that handles the onChange events 
and managing the state of the currently selected value. */}
<RadioImageForm>
  {/* The child, sub-components. 
  Each sub-component is an radio input displayed as an image
  where the user is able to click an image to select a value. */}
  <RadioImageForm.RadioInput />
  <RadioImageForm.RadioInput />
  <RadioImageForm.RadioInput />
</RadioImageForm>

В файле src/components/RadioImageForm.tsx мы имеем 1 основной компонент:

  1. RadioImageForm — Сначала мы создаем родительский компонент, который будет управлять состоянием и обрабатывать события изменения формы. Потребитель компонента, другие инженеры, использующие компонент, могут подписаться на текущее выбранное значение радиовходов, с помощью функции поддержки обратного вызова (callback function prop), onStateChange. При каждом изменении формы компонент будет обрабатывать обновления радиовходов и предоставлять потребителю текущее значение.

Внутри компонента RadioImageForm имеется один статический компонент или подкомпонент:

  1. RadioInput — Далее создадим статический компонент, элемент подмножества компонента RadioImageForm. RadioInput — это статический компонент, который вызывается через точечную запись в синтаксисе, например, <RadioImageForm.RadioInput/>. Это позволяет потребителю нашего компонента легко получить доступ к нашим подкомпонентам и позволяет ему иметь представление о том, как RadioInput отображается в форме.

Компонент RadioInput является статическим свойством класса RadioImageForm. Составной компонент состоит из родительского компонента RadioImageForm и статического компонента RadioInput. Далее я буду называть статические компоненты «подкомпонентами» ( "sub-components.").

Давайте сделаем первые шаги по созданию нашего компонента RadioImageForm.

export class RadioImageForm extends React.Component<Props, State> {

  static RadioInput = ({
    currentValue,
    onChange,
    label,
    value,
    name,
    imgSrc,
    key,
  }: RadioInputProps): React.ReactElement => (
    //...
  );

  onChange = (): void => {
    // ...
  };

  state = {
    currentValue: '',
    onChange: this.onChange,
    defaultValue: this.props.defaultValue || '',
  };

  render(): React.ReactElement {
    return (
      <RadioImageFormWrapper>
        <form>
        {/* .... */}
        </form>
      </RadioImageFormWrapper>
    )
  }
}

При создании многократно используемых компонентов мы хотим предоставить продукт, в котором потребитель имеет контроль над тем, где именно элементы отображаются в его коде.  Для корректной работы компонентов RadioInput потребуется доступ к внутреннему состоянию, внутренней функции onChange, а также к «пропсам» пользователя (user's props). Но как передать эти данные подкомпонентам? Здесь в игру вступают React.children.map и React.cloneElement. Для подробного объяснения того, как это работает вы можете углубиться в документацию React:

Конечный результат RadioImageForm при использовании метода рендеринга выглядит следующим образом:

render(): React.ReactElement {
  const { currentValue, onChange, defaultValue } = this.state;

  return (
    <RadioImageFormWrapper>
      <form>
        {
          React.Children.map(this.props.children, 
            (child: React.ReactElement) =>
              React.cloneElement(child, {
                currentValue,
                onChange,
                defaultValue,
              }),
          )
        }
      </form>
    </RadioImageFormWrapper>
  )
}

Следует отметить в этой имплементации:

  1. RadioImageFormWrapper — наши стили компонентов со стилизованными компонентами. Мы можем это проигнорировать, так как стили CSS не имеют отношения к шаблону компонентов.

  2. React.children.map — итерация выполняется через дочерние компоненты напрямую, что позволяет нам манипулировать каждым дочерним компонентом непосредственно.

  3. React.cloneElement — из документов React docs:

  • Клонируйте React-элемент и возвращайте новый, используйте его в качестве отправной точки. Полученный элемент будет иметь пропс (props) оригинала, который будет плавно объединен с новым пропсом. Новые дочерние элементы заменят существующие.

С помощью React.children.map и React.cloneElement мы можем выполнять итерацию и манипулировать каждым из дочерних элементов. Таким образом, мы можем передавать дополнительные пропсы, которые четко определяем в этом процессе трансформации. В этом случае мы можем передать внутреннее состояние RadioImageForm каждому дочернему компоненту RadioInput. Так как React.cloneElement выполняет мягкое слияние, то любой пропс, определенный пользователем на RadioInput, будет передан компоненту.

Наконец, мы можем объявить компонент статического свойства RadioInput в нашем классе RadioImageForm. Это позволяет пользователю вызывать наш компонент подмножества, RadioInput, непосредственно из RadioImageForm с помощью точечной записи в синтаксисе. Это помогает улучшить читабельность и явно объявляет подкомпоненты. С помощью этого интерфейса мы создали удобный и многократно используемый компонент. Вот наш статический компонент RadioInput:

static RadioInput = ({
  currentValue,
  onChange,
  label,
  value,
  name,
  imgSrc,
  key,
}: RadioInputProps) => (
  <label className="radio-button-group" key={key}>
    <input
      type="radio"
      name={name}
      value={value}
      aria-label={label}
      onChange={onChange}
      checked={currentValue === value}
      aria-checked={currentValue === value}
    />
    <img alt="" src={imgSrc} />
    <div className="overlay">
      {/* .... */}
    </div>
  </label>
);

Отметим, что в RadioInputProps мы однозначно определили в качестве образца, какие пропсы пользователь может передавать подкомпонентам RadioInput.

Затем пользователь компонента может ссылаться на RadioInput с помощью точечной записи синтаксиса (dot-syntax notation) в своем коде (RadioImageForm.RadioInput):

// src/index.tsx
<RadioImageForm onStateChange={onChange}>
  {DATA.map(
    ({ label, value, imgSrc }): React.ReactElement => (
      <RadioImageForm.RadioInput
        label={label}
        value={value}
        name={label}
        imgSrc={imgSrc}
        key={imgSrc}
      />
    ),
  )}
</RadioImageForm>

Поскольку RadioInput является статическим свойством, он не имеет доступа к элементу RadioImageForm. Следовательно, вы не можете напрямую ссылаться на состояние или методы, определённые в классе RadioImageForm. Например, this.onChange не будет работать в следующем примере: static RadioInput = () => <input onChange={this.onChange} //…

Заключение

С помощью этой гибкой философии мы абстрагировались от деталей реализации формы радиоизображения. Как бы ни была проста внутренняя логика нашего компонента, с помощью более сложных элементов мы можем освободить пользователя от внутренней работы. Родительский компонент, RadioImageForm, обрабатывает действия по изменению событий и обновлению текущего контроля радиовхода.  А подкомпонент RadioInput способен идентифицировать текущий выбранный вход. 

Мы предоставили базовую стилизацию для формы радиоизображения. Дополнительным бонусом является то, что мы также добавили доступ к нашим компонентам. Эта внутренняя логика компонента RadioImageForm для управления состоянием формы, с использованием текущего контроля радиовхода и применения стилей формы является деталями реализации, которая не должна касаться инженеров, использующих наш компонент.

Недостатки

В то время как мы создали удобный интерфейс для пользователей наших компонентов, тем не менее есть брешь в нашем проекте. Что если <RadioImageForm.RadioInput/> будет погребен в куче div-элементов? Что произойдет, если пользователь захочет переупорядочить компоновку? Компонент всё также отобразит, но радиовход не получит текущее значение из состояния RadioImageForm, что нарушит пользовательский функционал. Этот проект шаблона компонентов не является гибким, что подводит нас к нашему следующему варианту создания шаблона компонентов.

 Составные компоненты CodeSandBox

Пример составных компонентов с функциональными компонентами и React хуками (React hooks):

Составные компоненты и функциональные компоненты CodeSandBox


Гибкие компоненты соединения

Обзор

В нашем предыдущем примере мы использовали шаблон, составленный из компонентов, но что произойдет, когда мы обернем нашу подкомпоненту в кучу div-элементов? Он разобьётся. Он не является гибким. Проблема составных компонентов заключается в том, что они могут клонировать и передавать пропсы только ближайшим дочерним элементам.

Зачем использовать гибкие составные компоненты? Какую ценность они представляют?

С помощью гибких составных компонентов, мы можем неявно получить доступ к внутреннему состоянию компонента нашего класса, независимо от того, где они находятся в дереве компонентов. Другая причина использования гибких составных компонентов — это когда несколько компонентов должны иметь общее состояние, независимо от их положения в дереве компонентов. Пользователь компонента должен иметь возможность гибко выбирать, где будут рендерится наши составные компоненты. Для этого мы будем использовать React's Context API.

Но сначала мы должны получить некоторый контекст (context) о React's Context API, прочитав официальные React docs.

Пример

Мы продолжим на примере формы радиоизображения и рефакторинга компонента RadioImageForm для создания гибкого составного компонентного шаблона. Вы можете проследить за конечным результатом в CodeSandBox.

Давайте создадим некоторый контекст для нашего компонента RadioImageForm, чтобы мы могли передавать данные дочерним компонентам (например, RadioInput) в любом месте в родительском дереве компонентов. Будем надеяться, что вы почистили React's Context, но вот краткое резюме из документа React's doc:

  • Контекст обеспечивает способ передачи данных через дерево компонентов без необходимости передавать реквизит вручную на каждом уровне.

Во-первых, мы называем метод React.createContext, предоставляя значения по умолчанию в нашем контексте. Затем мы присваиваем отображаемое имя объекту контекста. Мы добавим его в верхнюю часть нашего файла RadioImageForm.tsx.

const RadioImageFormContext = React.createContext({
  currentValue: '',
  defaultValue: undefined,
  onChange: () => { },
});
RadioImageFormContext.displayName = 'RadioImageForm';
  1. Вызовом React.createContext мы создали контекстный объект, содержащий пару Provider и Consumer. Первые будут предоставлять данные вторым; в нашем примере Provider будет показывать наше внутреннее состояние подкомпонентам.

  2. Назначив displayName нашему контекстному объекту, мы можем легко различать компоненты контекста в React Dev Tool (React Developer Tools). Таким образом, вместо Context.Provider или Context.Consumer у нас будут RadioImageForm.Provider и RadioImageForm.Consumer. Это помогает повысить удобство чтения, если у нас есть несколько компонентов, использующих Context во время отладки.

Далее мы можем рефакторизовать рендер-функцию компонента RadioImageForm и удалить из него устаревшие функции React.children.map и React.cloneElement, а также выполнить рендеринг пропсов дочерних элементов.

render(): React.ReactElement {
  const { children } = this.props;

  return (
    <RadioImageFormWrapper>
      <RadioImageFormContext.Provider value={this.state}>
        {children}
      </RadioImageFormContext.Provider>
    </RadioImageFormWrapper>
  );
}

RadioImageFormContext.Provider принимает один проп (prop-свойство) с именем value. Данные, передаваемые в проп — это контекст, который мы хотим предоставить потомкам (descendants) этого Provider.  Подкомпонентам необходим доступ к нашему внутреннему состоянию, а также к внутренней функции onChange. Назначив метод onChange, currentValue и defaultValue объекту state, мы можем передать this.state в контекстное значение.

  • ? Всякий раз, когда value меняется на что-то другое, оно осуществляет ререндеринг себя и всех своих потребителей. React - это постоянный рендеринг, поэтому, передавая объект в пропс value, он будет ререндерить (re-render) все дочерние компоненты, потому что объект переназначается на каждом рендеринге (созданием нового объекта). Это неизбежно может привести к проблемам с производительностью, потому что переданный в объект пропс value будет воссоздаваться каждый раз, когда дочерняя компонента ререндируется (re-renders) даже в том случае, если значения в объекте не изменились. НЕ ДЕЛАЙТЕ ЭТОГО: <RadioImageFormContext.Provider value={{ currentValue: this.state.currentValue, onChange: this.onChange }}>. Вместо этого передайте this.state, чтобы предотвратить лишний ререндеринг дочерних компонентов.

И, наконец, наши подкомпоненты могут потреблять предоставленный контекст, наши внутренние данные, которые мы только что создали ранее. Так как все наши подкомпоненты являются внутренними в нашем компоненте RadioImageForm, мы можем определить Consumer как статическое свойство RadioImageForm.

export class RadioImageForm extends React.Component<Props, State> {
  static Consumer = RadioImageFormContext.Consumer;
  //...

В качестве альтернативы, если у вас есть внешние компоненты, которые должны быть подписаны на контекст, вы можете экспортировать RadioImageFormContext.Consumer в файл, например, экспортировать const RadioImageFormConsumer = RadioImageFormContext.Consumer.

Для каждого из наших субкомпонентов мы можем объявить Consumer с помощью записи точечного синтаксиса, ререндируя потребителя в качестве корневого элемента.

Например, мы создадим кнопку отправки, где пользователь может выполнить функцию обратного вызова, с помощью которой мы сможем передать текущее значение currentValue, предоставленное из нашего контекстного значения.  В нашей RadioImageForm мы создадим компонент SubmitButton.

static SubmitButton = ({ onSubmit }: SubmitButtonProps) => (
  <RadioImageForm.Consumer>
    {({ currentValue }) => (
      <button
        type="button"
        className="btn btn-primary"
        onClick={() => onSubmit(currentValue)}
        disabled={!currentValue}
        aria-disabled={!currentValue}
      >
        Submit
      </button>
    )}
  </RadioImageForm.Consumer>
);

Следует отметить, что Consumer требует функцию в качестве дочерней; он использует шаблон render props, например ({ currentValue }) => (// Render content)). Эта функция получает текущее контекстное значение, подписываясь на изменения внутреннего состояния. Это позволяет нам явно указывать, какие данные нам нужны от Provider. Например, SubmitButton ожидает свойство currentValue, которое было ссылкой на класс RadioImageForm. Но теперь он получает прямой доступ к этим значениям через Context.

Для лучшего понимания того, как работает рендеринг пропсов (функция в качестве дочерней концепции), вы можете посетить React Docs.

Благодаря этим изменениям пользователь нашего компонента может использовать наши составные компоненты (compound components) в любом месте дерева компонентов (component tree). 

В файле src/index.tsx вы можете посмотреть, как потребитель нашего компонента может его использовать.

Заключение

Благодаря такой схеме мы можем разрабатывать компоненты, пригодные для многократного применения, при этом потребитель может гибко использовать наши компоненты в различных контекстах. Мы обеспечили удобный интерфейс, в котором потребитель компонента не нуждается в знании внутренней логики. С помощью Context API мы можем передать неявное состояние нашего компонента подкомпонентам независимо от их глубины в иерархии. Это дает пользователю контроль для улучшения их стилистического восприятия. И в этом вся прелесть компонентов Flexible Compound Components: они помогают отделить презентацию от внутренней логики. Реализация составных компонентов с помощью Контекстного API является более выгодной, и поэтому я бы рекомендовал начинать с шаблона «Гибкие составные компоненты», а не «Составные компоненты».

Гибкий составной компонент CodeSandBox

Пример гибких составных компонентов с функциональными компонентами и React hooks:

Гибкие составные компоненты и Функциональные компоненты CodeSandBox


Шаблон Provider

Обзор

Шаблон Провайдера (provider pattern) является элегантным решением для совместного использования данных в дереве компонентов React. Шаблон провайдера использует предыдущие концепции, две основные из которых — контекстный API в React и рендеринг пропсов.

Для получения более подробной информации посетите React docs on Context API и Render Props.

Context API:

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

Рендеринг Пропсов (Render Props):

  • Термин "render prop" относится к технике разделения кода между React-компонентами, использующей реквизит (prop), значение которого является функцией.

Зачем использовать шаблон провайдера (provider pattern)? Какую ценность он представляет?

Шаблон провайдера — это мощная концепция, которая помогает при проектировании сложного приложения, так как решает несколько задач. С помощью React мы имеем дело с однонаправленным потоком данных, и при объединении нескольких компонентов мы должны обеспечить пробрасывание пропов (prop drill) общего состояния от родительского уровня к дочерним. Это может привести к неприглядному спагетти-коду (spaghetti code).

Проблема загрузки и отображения общих данных на странице заключается в обеспечении этого общего состояния для дочерних компонентов, которые нуждаются в доступе к нему. С помощью React's Context API мы можем создать компонент поставщика данных, который будет заниматься извлечением данных и предоставлением общего состояния для всего дерева компонентов. Таким образом, несколько дочерних компонентов, независимо от того, насколько глубоко они расположены, могут получить доступ к одним и тем же данным. Сбор данных и отображение данных — это две отдельные задачи. В идеале, один компонент имеет только одну задачу. Родительский компонент, обертывающий данные (провайдер), в первую очередь отвечает за сбор данных и обработку общего состояния, в то время как дочерние компоненты могут сосредоточиться на том, как рендировать эти данные. Компонент провайдера также может обрабатывать бизнес-логику нормализации (normalizing) и массирования (massaging) данных при отклике (response data), чтобы дочерние компоненты последовательно получали одну и ту же модель даже при обновлении конечных точек API и изменении модели отклика данных (response data model). Такое разделение обязанностей имеет большое значение при построении больших приложений, так как помогает в обслуживании и упрощает разработку. Другие разработчики могут легко определить сферу ответственности каждого компонента.

Некоторые могут задаться вопросом, почему бы не использовать библиотеку управления состоянием, такую как Redux, MobX, Recoil, Rematch, Unstated, Easy Peasy, или ряд других? Хотя эти библиотеки могут помочь в решении проблемы управления состоянием, нет необходимости в чрезмерном совершенствовании технологии решения этой проблемы. Внедрение библиотеки управления состоянием создает массу повторяющегося кода шаблонов, сложных потоков, которые необходимо изучить другим разработчикам, а также раздувает приложение, что увеличивает его размер. Я не говорю вам, что библиотека управления состоянием бесполезна, и что вы не должны ее использовать, но важно точно знать, какое преимущество она сможет обеспечить, чтобы обосновать использование новой библиотеки. Когда я инициализировал свое приложение с помощью React, я отказался от использования библиотеки управления состоянием, несмотря на то, что так происходило во всех других проектах React. Хотя мои потребности могут отличаться от других требований, я не увидел причин усложнять нашу кодовую базу (codebase) при помощи инструмента управления состоянием, который, возможно, придется освоить будущим разработчикам. Вместо этого я выбрал решение с использованием шаблона провайдера.

Пример

После этого длительного вступления, давайте рассмотрим пример. На этот раз мы создадим очень простое приложение, чтобы продемонстрировать, как мы можем легко делиться состоянием между компонентами и даже страницами, при этом придерживаясь таких принципов проектирования, как разделение проблем (SoC) и DRY (Don't Repeat Yourself). Вы можете проследить за конечным результатом в CodeSandBox. В нашем примере, мы создадим социальное приложение для собак, где наш пользователь сможет просматривать их профиль и список друзей собак.

Сначала создадим компонент провайдера данных, DogDataProvider, который будет отвечать за получение наших данных и предоставление их дочерним компонентам, независимо от их положения в дереве компонентов, с помощью контекстного API React's Context.

// src/components/DogDataProvider.tsx
interface State {
  data: IDog;
  status: Status;
  error: Error;
}

const initState: State = { status: Status.loading, data: null, error: null };

const DogDataProviderContext = React.createContext(undefined);
DogDataProviderContext.displayName = 'DogDataProvider';

const DogDataProvider: React.FC = ({ children }): React.ReactElement => {
  const [state, setState] = React.useState<State>(initState);

  React.useEffect(() => {
    setState(initState);

    (async (): Promise<void> => {
      try {
        // MOCK API CALL
        const asyncMockApiFn = async (): Promise<IDog> =>
          await new Promise(resolve => setTimeout(() => resolve(DATA), 1000));
        const data = await asyncMockApiFn();

        setState({
          data,
          status: Status.loaded,
          error: null
        });
      } catch (error) {
        setState({
          error,
          status: Status.error,
          data: null
        });
      }
    })();
  }, []);

  return (
    <DogDataProviderContext.Provider value={state}>
      {children}
    </DogDataProviderContext.Provider>
  );
};

Примечательно в этой имплементации:

  1. Сначала мы создаем контекстный объект DogDataProviderContext с помощью React Context API через React.createContext. Это будет использоваться для обеспечения состояния потребляющих компонентов с помощью пользовательского React's хук (hook), который мы применим позже.

  2. Назначив displayName нашему контекстному объекту, мы сможем легко различать компоненты контекста в React Dev Tool. Поэтому вместо Context.Provider в React Dev Tools мы будем использовать DogDataProvider.Provider. Это поможет повысить удобство чтения, если во время отладки мы используем несколько компонентов, использующих Context.

  3. В нашем хуке useEffect мы будем извлекать и управлять те же общие данными, которые будут потреблять несколько дочерних компонентов.

  4. Модель нашего состояния включает в себя наше креативно названное свойство данных, свойство статуса и свойство ошибки. С помощью этих трех свойств дочерние компоненты могут решать, какие состояния отображать: 1. состояние загрузки, 2. состояние загрузки с рендированными данными или 3. состояние ошибки.

  5. Так как мы отделили загрузку и управление данными от тех компонентов пользовательского интерфейса (UI), которые заботятся об их отображении, у нас не будет лишних извлечений данных, когда компоненты пользовательского интерфейса монтируются и демонтируются.

Далее мы создадим наш пользовательский React hook в том же файле, в котором мы создали компонент DogDataProvider. Пользовательский хук (hook) будет предоставлять контекстное состояние от компонента DogDataProvider к потребляющим компонентам.

// src/components/DogDataProvider.tsx

export function useDogProviderState() {
  const context = React.useContext(DogDataProviderContext);

  if (context === undefined) {
    throw new Error('useDogProviderState must be used within DogDataProvider.');
  }

  return context;
}

Пользовательский хук использует [React.useContext](https://reactjs.org/docs/hooks-reference.html#usecontext) для получения предоставленного контекстного значения из компонента DogDataProvider, и он вернет состояние контекста, когда мы его вызовем. Выставляя пользовательский хук, компоненты-потребители могут подписаться на состояние, которое управляется в компоненте данных провайдера.

Также мы добавили обработку ошибок, если хук вызывается в компоненте, который не является потомком компонента провайдера данных. Это гарантирует в случае неправильного использования, что он быстро выйдет из строя и предоставит важное сообщение об ошибке.

Наконец, мы отображаем данные при загрузке в потребляющие компоненты. Сконцентрируемся на компоненте Profile, который загружается по маршруту личного каталога (home path), но вы также можете посмотреть примеры потребительских компонентов в DogFriends и Nav компонентах.

Сначала в файле index.tsx мы должны обернуть компонент DogDataProvider на корневом уровне (root level):

// src/index.tsx
function App() {
  return (
    <Router>
      <div className="App">
        {/* The data provder component responsible 
        for fetching and managing the data for the child components.
        This needs to be at the top level of our component tree.*/}
        <DogDataProvider>
          <Nav />
          <main className="py-5 md:py-20 max-w-screen-xl mx-auto text-center text-white w-full">
            <Banner
              title={'React Component Patterns:'}
              subtitle={'Provider Pattern'}
            />
            <Switch>
              <Route exact path="/">
                {/* A child component that will consume the data from 
                the data provider component, DogDataProvider. */}
                <Profile />
              </Route>
              <Route path="/friends">
                {/* A child component that will consume the data from 
                the data provider component, DogDataProvider. */}
                <DogFriends />
              </Route>
            </Switch>
          </main>
        </DogDataProvider>
      </div>
    </Router>
  );
}

Затем в компоненте Profile мы можем использовать пользовательский хук

useDogProviderState:

const Profile = () => {
  // Our custom hook that "subscirbes" to the state changes in 
  // the data provider component, DogDataProvider.
  const { data, status, error } = useDogProviderState();

  return (
    <div>
      <h1 className="//...">Profile</h1>
      <div className="mt-10">
        {/* If the API call returns an error we will show an error message */}
        {error ? (
          <Error errorMessage={error.message} />
          // Show a loading state when we are fetching the data
        ) : status === Status.loading ? (
          <Loader isInherit={true} />
        ) : (
          // Display the content with the data 
          // provided via the custom hook, useDogProviderState.
          <ProfileCard data={data} />
        )}
      </div>
    </div>
  );
};

Следует отметить в этой имплементации:

  1. При получении данных мы покажем состояние загрузки.

  2. Если API запрос вернет ошибку, то мы покажем сообщение об ошибке.

  3. Наконец, после того, как данные будут получены и предоставлены через пользовательский хук, useDogProviderState, мы рендерируем компонент ProfileCard.

Заключение

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

Шаблон Provider с пользовательским примером

Поскольку React хуки были представлены для React v16.8, но если вам нужна поддержка версий ниже v16.8, то здесь представлен тот же пример без хуков: CodeSandBox.


В ближайшие дни в OTUS стартует сразу несколько курсов по JavaScript разработке. Узнайте подробнее о курсах по ссылкам ниже:

- JavaScript Developer. Basic

- JavaScript Developer. Professional

- React.js Developer

Tags:
Hubs:
+4
Comments1

Articles

Information

Website
otus.ru
Registered
Founded
Employees
101–200 employees
Location
Россия
Representative
OTUS