React course
  • Компоненты и коллекции
  • TypeScript
  • Стилизация
  • События и состояния
  • Формы
  • Жизненный цикл
  • Функциональные vs классовые компоненты
  • Основы Redux
  • Redux Toolkit
  • Асинхронный Redux
  • Селекторы
  • React Router
  • Code splitting
  • Паттерны и контекст
  • Анимация
Powered by GitBook
On this page
  • 1. Паттерны
  • 1.1. Higher-Order Component
  • 1.2. Render Prop
  • 1.3. Дополнительные материалы
  • 2. Context API
  • 2.1. React.createContext()
  • 2.2. Provider
  • 2.3. Consumer
  • 2.4. Контекст темы
  • 2.5. HOC для подписки на контекст
  • 2.6. Производительность

Was this helpful?

Паттерны и контекст

1. Паттерны

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

1.1. Higher-Order Component

HOC  — функция, которая принимает компонент как аргумент и возвращает новый компонент (функцию или класс) с расширенным функционалом.

  • Применяется когда нужно использовать повторяющуюся логику, применяемую к ряду компонентов, тем самым дополнив их функционал.

  • HOC должен быть чистой функцией без побочных эффектов.

  • HOC нельзя использовать в методе render(). Композиция должна быть статической, то есть во время экспорта компонента.

1.1.1. Создание и использование

// withHigherOrderComponent.js
import React, { Component } from 'react';

const withHigherOrderComponent = WrappedComponent => {
  return class WithHigherOrderComponent extends Component {
    render() {
      return (
        <WrappedComponent {...this.props} extraProp="This prop is from HOC" />
      );
    }
  };
};

export default withHigherOrderComponent;

Использование компонента высшего порядка называется статическая композиция, потому что создание обертки происходит один раз, при экспорте оборачиваемого компонента.

// MyComponent.js
import React from 'react';
import withHigherOrderComponent from '/path/to/withHigherOrderComponent';

const MyComponent = props => <div>{JSON.stringify(props, null, 2)}</div>;

export default withHigherOrderComponent(MyComponent);

1.1.2. withLog

HOC который просто логирует все пропсы полученные компонентом.

const withLog = WrappedComponent => {
  return class WithLog extends Component {
    componentDidMount() {
      console.group(`WithLog ouput @${WrappedComponent.name}`);
      console.log(this.props);
      console.groupEnd();
    }

    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
};

1.1.3. withFetch

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

// withFetch.js
const withFetch = url => WrappedComponent => {
  return class WithFetch extends Component {
    state = {
      data: [],
      loading: false,
      error: null,
    };

    componentDidMount() {
      this.setState({ loading: true });

      fetch(url)
        .then(res => res.json())
        .then(data => this.setState({ data }))
        .catch(error => this.setState({ error }))
        .finally(() => this.setState({ loading: false }));
    }

    render() {
      return <WrappedComponent {...this.props} {...this.state} />;
    }
  };
};

export default withFetch;

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

// App.js
const App = props => {
  return <h1>В моих пропсах будут data, loading и error</h1>;
};

export default withFetch('https://jsonplaceholder.typicode.com/todos')(App);

1.1.4. withToggle

HOC позволяющий сделать переключаемым любой компонент.

const withToggle = WrappedComponent => {
  return class WithToggle extends Component {
    state = {
      isOpen: false,
    };

    toggle = () => this.setState(prevState => ({ isOpen: !prevState.isOpen }));

    render() {
      return (
        <>
          <button type="button" onClick={this.toggle}>
            {this.state.isOpen ? 'Hide' : 'Show'}
          </button>

          {isOpen && <WrappedComponent {...this.props} />}
        </>
      );
    }
  };
};

Получился HOC, который рендерит кнопку для скрытия или отображения контента. А что если мы хотим другой элемент управления? Тогда придется передать состояние isOpen и метод toggle пропсами в WrappedComponent. Это загрязняет его пропсы и приводит к проблеме которая называется prop collision. В случае когда необходимо рендерить разметку, лучше использовать паттерн Render Prop.

1.1.5. Дополнительные материалы

1.2. Render Prop

Идея использования паттерна заключается в передаче управления рендером другому компоненту, а сам Render Prop отвечает только за состояние и его обновление. Для этого в проп children передается функция.

1.2.1. Toggler

Компонент позволяющий сделать переключаемым любой другой компонент. <Toggler> управляет только изменением состояния, а то как это состояние будет использовано его не интересует.

// Toggler.js
class Toggler extends Component {
  state = {
    isOpen: false,
  };

  toggle = () => this.setState(state => ({ isOpen: !state.isOpen }));

  render() {
    return this.props.children({
      isOpen: this.state.isOpen,
      onToggle: this.toggle,
    });
  }
}

export default Toggler;

Использование паттерна Render Prop это динамическая композиция. <Toggler> открывает доступ к состоянию и методу для его изменения. Разработчик может использовать одинаковую логику для разных элементов интерфейса.

// App.js
const App = () => (
  <div>
    <Toggler>
      {({ isOpen, onToggle }) => (
        <>
          <button type="button" onClick={onToggle}>
            {isOpen ? 'Hide' : 'Show'}
          </button>
          {isOpen && <p>Vestibulum suscipit nulla quis orci.</p>}
        </>
      )}
    </Toggler>

    <Toggler>
      {({ isOpen, onToggle }) => (
        <>
          <label>
            <input type="checkbox" checked={isOpen} onChange={onToggle} />
            {isOpen ? 'Hide' : 'Show'}
          </label>
          {isOpen && <p>Etiam feugiat lorem non metus.</p>}
        </>
      )}
    </Toggler>
  </div>
);

1.2.2. Autocomplete и TagList

Пример повторного использования одной логики фильтрации для создания внешне различных компонентов.

1.2.3. Дополнительные материалы

1.3. Дополнительные материалы

2. Context API

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

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

Не используйте контекст чтобы избежать передачи пропсов на несколько уровней вниз.

2.1. React.createContext()

const Context = React.createContext(defaultValue);
  • Создает объект контекста содержащий пару компонентов: <Context.Provider> (поставщик) и <Context.Consumer> (потребитель).

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

  • Аргумент defaultValue используется потребителем, если у него нет соответствующего поставщика над ним в дереве.

2.1.1. Свойство displayName

Объекту полученному из функции React.createContext() можно задать строковое свойство displayName. React DevTools использует это свойство при отображении контекста.

К примеру, следующий компонент будет отображаться под именем ThemeContext в DevTools:

const ThemeContext = React.createContext();
ThemeContext.displayName = 'ThemeContext';

<ThemeContext.Provider> // "ThemeContext.Provider" в DevTools
<ThemeContext.Consumer> // "ThemeContext.Consumer" в DevTools

2.2. Provider

Компонент, позволяющий потребителям подписываться на изменения контекста. Используется для создания и передачи контекста.

<Provider value={/* value */}>
  • Принимает проп value - значение контекста, которое будет передано потомкам-потребителям этого контекста.

  • Позволяет потребителям подписываться на изменения контекста независмо от глубины вложености.

  • Один провайдер может быть связан со многими потребителями.

  • Провайдеры могут быть вложены друг в друга.

2.3. Consumer

Компонент, который подписывается на изменения контекста. Получает текущий контекст из ближайшего сопоставимого <Provider> выше в дереве.

<Consumer>
  {context => {
    /* Возвращает JSX-разметку */
  }}
</Consumer>
  • Релизован по патерну Render Prop, поэтому ожидает функцию в качестве дочернего элемента.

  • Параметр context, будет содержать значение контекста ближайшего провайдера для этого контекста выше в дереве.

  • Если для этого контекста нет провайдера, аргумент context будет содержать значение по умолчанию, которое было передано в React.createContext().

  • Все потребители, являющиеся потомками провайдера, будут ре-рендериться всякий раз, когда изменяется значение контекста (пропа value).

  • Потребитель обновляется даже тогда, когда компонент-предок выше в дереве отказался ре-рендериваться используя shouldComponentUpdate().

2.4. Контекст темы

import React, { createContext } from 'react';

const ThemeContext = createContext();

const App = () => (
  <ThemeContext.Provider value="light">
    <div className="App">
      <Toolbar />
    </div>
  </ThemeContext.Provider>
);

const Toolbar = () => (
  <div className="Toolbar">
    <Button label="Log In" />
    <Button label="Log Out" />
  </div>
);

const Button = ({ label }) => (
  <ThemeContext.Consumer>
    {theme => (
      <button
        className={theme === 'light' ? 'btn-light' : 'btn-dark'}
        type="button"
      >
        {label}
      </button>
    )}
  </ThemeContext.Consumer>
);

2.5. HOC для подписки на контекст

Чаще всего контекст потребляется многими компонентами, и явно оборачивать каждый компонент с помощью <Context.Consumer> не лучший подход. Создадим компонент высшего порядка для подписки на контекст.

// withTheme.js
import ThemeContext from '/path/to/ThemeContext';

// Эта функция принимает компонент...
const withTheme = WrappedComponent => {
  // ... возвращает другой компонент...
  return function WithTheme(props) {
    // ... который рендерит обернутый в Consumer компонент,
    // передавая тему как проп.
    return (
      <ThemeContext.Consumer>
        {theme => <WrappedComponent {...props} theme={theme} />}
      </ThemeContext.Consumer>
    );
  };
};

export default withTheme;

Теперь любой компонент, который зависит от контекста темы, может легко подписаться на него с помощью созданной нами функции withTheme.

import withTheme from '/path/to/withTheme';

const Button = ({ label, theme }) => (
  <button className={theme === 'dark' ? 'btn-dark' : 'btn-light'} type="button">
    {label}
  </button>
);

export default withTheme(Button);

2.6. Производительность

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

Например, приведенный ниже код будет повторно отрисовывать всех потребителей каждый раз, когда обновляется родитель провайдера, потому что для value всегда создается новый объект (новая ссылка).

class App extends React.Component {
  render() {
    return (
      <Provider value={{ something: 'some value' }}>
        <Toolbar />
      </Provider>
    );
  }
}

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

// ThemeContextProvider.js
import React, { Component, createContext } from 'react';

const { Provider, Consumer } = createContext();

export default class ThemeContextProvider extends Component {
  static Consumer = Consumer;

  toggleTheme = () => {
    this.setState(state => ({
      theme: state.theme === 'light' ? 'dark' : 'light',
    }));
  };

  state = {
    theme: 'light',
    toggleTheme: this.toggleTheme,
  };

  render() {
    return <Provider value={this.state}>{this.props.children}</Provider>;
  }
}
PreviousCode splittingNextАнимация

Last updated 4 years ago

Was this helpful?

Как и HOC, позволяет реализовать повторное использование логики. В большинстве случаев эти паттерны взаимозаменяемы. Плюс это отсутствие сайд-эффектов в обёрнутом компоненте. Минус это при использовании нескольких оберток в одном копоненте. Проблема читабельности в некоторой степени решается такими библиотеками как .

Документация по HOC
React Higher-Order Components от Tyler McGinnis
читабельность кода
react-adopt
Michael Jackson - Never Write Another HoC
React Render Props от Tyler McGinnis
Learn Render Props by Example
Code Reuse Patterns - Guy Romm
react-fns
Репозиторий recompose