Pull to refresh

Redux против React Context API

Reading time 15 min
Views 77K
Original author: Dave Ceddia


В React 16.3 был добавлен новый Context API. Новый в том смысле, что старый Context API был за кадром, большинство людей либо не знали о его существовании, либо не использовали, потому что документация советовала избегать его использования.

Однако теперь Context API является полноценной частью React, открытой для использования (не так, как раньше, официально).

Сразу после релиза React 16.3, появились статьи, в которых провозглашалась смерть Redux из-за нового Context API. Если бы вы спросили об этом у Redux, я думаю, он ответил бы — «сообщения о моей смерти сильно преувеличены».

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

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

Пример React приложения


Я собираюсь предположить, что у вас есть понимание принципов работы с состоянием в React (props & state), но если этого нет, у меня есть бесплатный 5-дневный курс, который поможет вам узнать об этом.

Давайте посмотрим на пример, который подведет нас к концепции, используемой в Redux. Мы начнем с простой версии React, а затем посмотрим, как она выглядит в Redux и, наконец с Context.



В этом приложении информация о пользователе отображается в двух местах: на панели навигации в правом верхнем углу и на боковой панели рядом с основным контентом.

(Вы можете заметить, что большое сходство с Twitter. Не случайно! Один из лучших способов оттачивать ваши навыки React это — копирование (создание реплик существующих сайтов / приложений).

Структура компонента выглядит так:



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

Затем, чтобы передать информацию о пользователе тем компонентам, которые в ней нуждаются, приложению должно передать его в Nav и Body. Они, в свою очередь, передадут его UserAvatar (ура!) И Sidebar. Наконец, Sidebar должен передать ее в UserStats.

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

import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";

const UserAvatar = ({ user, size }) => (
  <img
    className={`user-avatar ${size || ""}`}
    alt="user avatar"
    src={user.avatar}
  />
);

const UserStats = ({ user }) => (
  <div className="user-stats">
    <div>
      <UserAvatar user={user} />
      {user.name}
    </div>
    <div className="stats">
      <div>{user.followers} Followers</div>
      <div>Following {user.following}</div>
    </div>
  </div>
);

const Nav = ({ user }) => (
  <div className="nav">
    <UserAvatar user={user} size="small" />
  </div>
);

const Content = () => <div className="content">main content here</div>;

const Sidebar = ({ user }) => (
  <div className="sidebar">
    <UserStats user={user} />
  </div>
);

const Body = ({ user }) => (
  <div className="body">
    <Sidebar user={user} />
    <Content user={user} />
  </div>
);

class App extends React.Component {
  state = {
    user: {
      avatar:
        "https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b",
      name: "Dave",
      followers: 1234,
      following: 123
    }
  };

  render() {
    const { user } = this.state;

    return (
      <div className="app">
        <Nav user={user} />
        <Body user={user} />
      </div>
    );
  }
}

ReactDOM.render(<App />, document.querySelector("#root"));


Пример кода на CodeSandbox

Здесь App инициализирует state, содержащий объект “user”. В реальном приложении вы скорее всего извлечете эти данные с сервера и сохраните их в state для рендеринга.

Что касается пробрасывания props (“prop drilling”), это не страшно. Оно работает отлично. «Пробрасывание props'ов», это — идеальный образец работы React. Но пробрасывание в глубину дерева состояния может немного раздражать при написании. И раздражать все более, если вам приходится передавать множество props'ов (а не один).

Тем не менее, существует большой минус в этой стратегии: она создает связь между компонентами, которые связанными быть не должны. В приведенном выше примере Nav должен принять “user” prop и передать его в UserAvatar, даже если Nav в нем не нуждается.

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

Давайте посмотрим, как мы можем улучшить это.

Перед тем как использовать Context или Redux...


Если вы сможете найти способ объединить структуру своего приложения и воспользоваться преимуществами передачи props потомкам, это может сделать ваш код чище, без необходимости прибегать к глубокому пробросу props, Context, или Redux.

В этом примере children props — отличное решение для компонентов, которые должны быть универсальными, такими как Nav, Sidebar и Body. Также знайте, что вы можете передавать JSX в любой prop, а не только в children — поэтому, если вам нужно больше одного «слота» для подключения компонентов, помните об этом.

Вот пример React-приложения, в котором Nav, Body и Sidebar принимают дочерние элементы и отображают их как есть. Таким образом, тому, кто использует компонент не нужно беспокоиться о передаче определенных данных, которые требуется компоненту. Он может просто отобразить то, что ему нужно, по месту, используя данные, которые он уже имеет в области видимости. В этом примере также показано, как использовать любой prop для передачи детей.

(Спасибо Дэну Абрамову за это предложение!)

import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";

const UserAvatar = ({ user, size }) => (
  <img
    className={`user-avatar ${size || ""}`}
    alt="user avatar"
    src={user.avatar}
  />
);

const UserStats = ({ user }) => (
  <div className="user-stats">
    <div>
      <UserAvatar user={user} />
      {user.name}
    </div>
    <div className="stats">
      <div>{user.followers} Followers</div>
      <div>Following {user.following}</div>
    </div>
  </div>
);

// Получаем children и рендерим их.
const Nav = ({ children }) => (
  <div className="nav">
    {children}
  </div>
);

const Content = () => (
  <div className="content">main content here</div>
);

const Sidebar = ({ children }) => (
  <div className="sidebar">
    {children}
  </div>
);

// Body должен содержать sidebar и content, но следуя такому подходу, 
// их может не быть.
const Body = ({ sidebar, content }) => (
  <div className="body">
    <Sidebar>{sidebar}</Sidebar>
    {content}
  </div>
);

class App extends React.Component {
  state = {
    user: {
      avatar:
        "https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b",
      name: "Dave",
      followers: 1234,
      following: 123
    }
  };

  render() {
    const { user } = this.state;

    return (
      <div className="app">
        <Nav>
          <UserAvatar user={user} size="small" />
        </Nav>
        <Body
          sidebar={<UserStats user={user} />}
          content={<Content />}
        />
      </div>
    );
  }
}

ReactDOM.render(<App />, document.querySelector("#root"));


Пример кода на CodeSandbox

Если ваше приложение слишком сложное (сложнее, чем этот пример!), Может быть, сложно понять как адаптировать шаблон под children. Посмотрим, как вы можете заменить проброс props с помощью Redux.

Пример на Redux


Я быстро рассмотрю пример на Redux, чтобы мы могли глубже понять, как работает Context, поэтому, если у вас нет четкого понимания о работе Redux, сначала прочитайте мое введение в Redux (или посмотрите видео).

Вот наше React-приложение, переделанное для использования Redux. Информация о пользователе была перемещена в store Redux, а это означает, что мы можем использовать функцию react-redux connect для непосредственной передачи user prop в компоненты, которые в них нуждаются.

Это большая победа в плане избавления от связанности. Взгляните на Nav, Body и Sidebar, и вы увидите, что они больше не принимают и не передают user prop. Больше не играют в «горячую картошку» с props’ами. Больше никаких бесполезных связей.

Редьюсер здесь мало что делает; это довольно просто. У меня есть еще кое-что о том, как работают редьюсеры Redux и как писать иммутабельный код, который в них используется.

import React from "react";
import ReactDOM from "react-dom";

// Нам нужны функции createStore, connect, and Provider:
import { createStore } from "redux";
import { connect, Provider } from "react-redux";

// Создаем reducer с пустым объектом в качестве начального состояния.
const initialState = {};
function reducer(state = initialState, action) {
  switch (action.type) {
    // В ответ на action SET_USER изменяем state.   
    case "SET_USER":
      return {
        ...state,
        user: action.user
      };
    default:
      return state;
  }
}

// Создаем store с reducer'ом в качестве аргумента.
const store = createStore(reducer);

// Dispatch'им action для того чтоб задать user.
store.dispatch({
  type: "SET_USER",
  user: {
    avatar: "https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b",
    name: "Dave",
    followers: 1234,
    following: 123
  }
});

// Это функция mapStateToProps, она извлекает один ключ из state (user) 
// и передает его как `user` prop.
const mapStateToProps = state => ({
  user: state.user
});

// Подключаем UserAvatar с помощью функции connect(), теперь он получает 
//`user` напрямую, без необходимости передавать от родительского компонента.

// вы можете разделить на 2 переменные:
//   const UserAvatarAtom = ({ user, size }) => ( ... )
//   const UserAvatar = connect(mapStateToProps)(UserAvatarAtom);
const UserAvatar = connect(mapStateToProps)(({ user, size }) => (
  <img
    className={`user-avatar ${size || ""}`}
    alt="user avatar"
    src={user.avatar}
  />
));

// Также подключаем UserStats с помощью функции connect(), теперь он получает 
// `user` напрямую.
const UserStats = connect(mapStateToProps)(({ user }) => (
  <div className="user-stats">
    <div>
      <UserAvatar />
      {user.name}
    </div>
    <div className="stats">
      <div>{user.followers} Followers</div>
      <div>Following {user.following}</div>
    </div>
  </div>
));

// Теперь у компонента Nav больше нет необходимости знать о `user`.
const Nav = () => (
  <div className="nav">
    <UserAvatar size="small" />
  </div>
);

const Content = () => (
  <div className="content">main content here</div>
);

// Как и Sidebar.
const Sidebar = () => (
  <div className="sidebar">
    <UserStats />
  </div>
);

// Как и Body.
const Body = () => (
  <div className="body">
    <Sidebar />
    <Content />
  </div>
);

// В App теперь не храниться состояния, он может быть чистой функцией.
const App = () => (
  <div className="app">
    <Nav />
    <Body />
  </div>
);

// Обернем все приложение в Provider, 
// теперь у connect() есть доступ к store.
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.querySelector("#root")
);


Пример кода на CodeSandbox

Теперь вам наверно интересно, как Redux достигает этой магии. Удивительно. Как React не поддерживает передачу props на несколько уровней, а Redux может это сделать?

Ответ заключается в том, что Redux использует context функцию React (context feature). Не современный Context API (еще нет), а старый. Тот, который в документации React сказано не использовать, если вы не пишете свою библиотеку или не знаете, что делаете.

Контекст похож на компьютерную шину, идущую за каждым компонентом: чтобы получить мощность (данные), проходящую через нее, вам нужно только подключиться. И react-redux connect делает именно это.

Однако эта особенность Redux — лишь верхушка айсберга. Передача данных лишь в необходимое место является наиболее очевидной из особенностей Redux. Вот несколько других преимуществ, которые вы получаете из коробки:

connect – чистая функция

connect автоматически делает подключенные компоненты «чистыми», то есть они будут повторно рендериться только при изменении их props — тоесть, когда изменяется их срез состояния Redux. Это предотвращает ненужный ре-рендер и ускоряет работу приложения.

Легкая отладка с помощью Redux

Церемония написания actions и reducers уравновешивается удивительной легкостью отладки, которую Redux вам предоставляет.

С расширением Redux DevTools вы получаете автоматический журнал всех actions, выполняемых вашим приложением. В любое время вы можете открыть его и посмотреть, какие actions были запущены, какой у них payload, и state до и после action’а.



Еще одной замечательной возможностью, которую предоставляет Redux DevTools является отладка c помощью «путешествий во времени», тоесть вы можете нажать на любой предыдущий action, и перейти к этому моменту времени, вплоть до текущего. Причина, по которой это работает, состоит в том, что каждый action одинаково обновляет store, поэтому вы можете взять список записанных обновлений состояния и воспроизвести их без каких-либо побочных эффектов, и закончить в нужном вам месте.

Также есть такие инструменты, как LogRocket, которые в основном дают вам постоянно действующий Redux DevTools в продакшене для каждого из ваших пользователей. Получили bug report? Не проблема. Посмотрите эту сессию пользователя в LogRocket, и вы можете увидеть повторение того, что он сделал, и какие именно actions были запущены. Все это работает, используя поток action’ов Redux.

Расширяем Redux с Middleware

Redux поддерживает концепцию middleware (причудливое слово, обозначающее «функцию, которая запускается каждый раз при отправке action’а»). Написание собственной middleware не так сложно, как может показаться, и позволяет использовать некоторые мощные средства.

Например…

  • Хотите посылать API-запрос каждый раз, когда имя action’a начинается с FETCH_? Вы можете сделать это с помощью middleware.
  • Хотите централизованное место для логирования событий в вашем аналитическом ПО? Middleware — хорошее место для этого.
  • Хотите предотвратить запуск action’a в определенный момент времени? Вы можете сделать это с помощью middleware, невидимого для остальной части вашего приложения.
  • Хотите перехватить action, имеющий токен JWT, и автоматически сохранить его в localStorage? Да, middleware.

Вот хорошая статья с примерами того, как писать Redux middleware.

Как использовать React Context API


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

Новый Context API, вероятно, подойдет вам. Давайте посмотрим, как он работает.

Я опубликовал быстрый урок по Context API на Egghead, если вам больше нравиться смотреть, чем читать (3:43).

Вот 3 важных составляющих Context API:

  • Функция React.createContext, которая создает context
  • Provider (возвращается createContext), который устанавливает «электрическую шину»,
    проходящую через дерево компонентов
  • Consumer (также возвращается createContext), который впитывается в
    «электрическую шину» для извлечения данных

Provider очень похож на Provider в React-Redux. Он принимает значение, которое может быть всем, чем хотите (это может быть даже store Redux… но это было бы глупо). Скорее всего, это объект, содержащий ваши данные и любые actions, которые вы хотите выполнить с данными.

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

Вот основные моменты:

// Для начала создаем новый context
// Это объект с 2 свойствами: { Provider, Consumer }
// Заметим, что они именованы, используя UpperCase, не camelCase
// Это важно, так как мы будем использовать из как компоненты в дальнейшем,
// а имена компонентов должны начинаться с большой буквы.
const UserContext = React.createContext();

// Компоненты, которым необходимы данные из context,
// используют свойство Consumer. 
// Consumer использует паттерн "render props".
const UserAvatar = ({ size }) => (
  <UserContext.Consumer>
    {user => (
      <img
        className={`user-avatar ${size || ""}`}
        alt="user avatar"
        src={user.avatar}
      />
    )}
  </UserContext.Consumer>
);

// Подчеркну, что нам больше не нужен "user prop",
// так как Consumer получает его из context.
const UserStats = () => (
  <UserContext.Consumer>
    {user => (
      <div className="user-stats">
        <div>
          <UserAvatar user={user} />
          {user.name}
        </div>
        <div className="stats">
          <div>{user.followers} Followers</div>
          <div>Following {user.following}</div>
        </div>
      </div>
    )}
  </UserContext.Consumer>
);

// ... здесь остальные компоненты ...
// ... (им больше не нужно беспокоиться о `user`).

// Внутри App мы передаем context вниз, используя Provider.
class App extends React.Component {
  state = {
    user: {
      avatar:
        "https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b",
      name: "Dave",
      followers: 1234,
      following: 123
    }
  };

  render() {
    return (
      <div className="app">
        <UserContext.Provider value={this.state.user}>
          <Nav />
          <Body />
        </UserContext.Provider>
      </div>
    );
  }
}


Пример кода на CodeSandbox

Давайте рассмотрим, как это работает.

Помните, у нас есть 3 части: сам context (созданный с помощью React.createContext) и два компонента, которые с ним взаимодействуют (Provider и Consumer).

Provider и Consumer работают вместе

Provider и Consumer связаны друг с другом и неотделимы. Они знают только, как взаимодействовать друг с другом. Если вы создали два отдельных context, скажем, «Сontext1» и «Сontext2», тогда Provider и Consumer Сontext1 не смогут общаться с Provider’ом и Consumer’ом Context2.

Контекст не содержит state

Обратите внимание, что context не имеет собственного state. Это всего лишь канал для ваших данных. Вы должны передать значение в Provider, и это значение передастся в любой Consumer, который знает, как его искать (Provider, привязан к тому же контексту, что и Consumer).

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

const Ctx = React.createContext(yourDefaultValue);


Значение по умолчанию это — то, что получит Consumer, когда будет помещен в дерево без Provider’a над ним. Если вы не передадите его, значение будет undefined. Обратите внимание, что это значение по умолчанию, а не начальное значение. Сontext ничего не сохраняет; он просто распространяет данные, которые вы в него передаете.

Consumer использует паттерн Render Props

Функция connect Redux это — компонент высшего порядка (сокращенно HoC). Она обворачивает другой компонент и передает в него props.

Consumer, напротив, ожидает, что дочерний компонент будет функцией. Затем он вызывает эту функцию во время рендеринга, передавая значение, полученное им от Provider’a где-то над ним (или значение по умолчанию из context, или undefined, если вы не передали значение по умолчанию).

Provider принимает одно значение

Просто одно значение, как prop. Но помните, что значение может быть любым. На практике, если вы хотите передать несколько значений вниз, вы должны создать объект со всеми значениями и передать этот объект вниз.

Context API гибкий


Поскольку создание контекста дает нам два компонента для работы с (Provider и Consumer), мы можем использовать их как мы хотим. Вот пара идей.

Оберните Consumer в HOC

Не нравится идея добавления UserContext.Consumer вокруг каждого места, которое в нем нуждается? Это ваш код! Вы вправе решать, что для вас будет лучшим выбором.

Если вы предпочитаете получать значение в качестве prop, вы можете написать небольшую обертку вокруг Consumer’a следующим образом:

function withUser(Component) {
  return function ConnectedComponent(props) {
    return (
      <UserContext.Consumer>
        {user => <Component {...props} user={user}/>}
      </UserContext.Consumer>
    );
  }
}

После этого, вы можете переписать, например UserAvatar с использованием функции withUser:

const UserAvatar = withUser(({ size, user }) => (
  <img
    className={`user-avatar ${size || ""}`}
    alt="user avatar"
    src={user.avatar}
  />
));

И вуаля, context может работать так же, как connect Redux’a. Минус автоматическая чистота.

Вот пример CodeSandbox с этим HOC.

Держите State в Provider’e

Помните, что Provider — это просто канал. Он не сохраняет никаких данных. Но это не мешает вам сделать свою собственную обертку для хранения данных.

В приведенном выше примере данные хранятся в App, так что единственное, что вам нужно было понять, это компоненты Provider + Consumer. Но, возможно, вы хотите создать свой собственный store. Вы можете создать компонент для хранения состояния и передачи их через контекст:

class UserStore extends React.Component {
  state = {
    user: {
      avatar:
        "https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b",
      name: "Dave",
      followers: 1234,
      following: 123
    }
  };

  render() {
    return (
      <UserContext.Provider value={this.state.user}>
        {this.props.children}
      </UserContext.Provider>
    );
  }
}

// ... пропускаем среднюю часть ...

const App = () => (
  <div className="app">
    <Nav />
    <Body />
  </div>
);

ReactDOM.render(
  <UserStore>
    <App />
  </UserStore>,
  document.querySelector("#root")
);

Теперь данные о пользователе содержатся в своем собственном компоненте, единственной задачей которого являются эти данные. Круто. App снова может стать чистым (stateless). Я думаю, что это и выглядит немного чище.

Вот пример CodeSandbox с этим UserStore.

Прокидываем actions вниз через context

Помните, что объект, передаваемый через Provider, может содержать все, что вы хотите. Это означает, что он может содержать функции. Вы можете даже назвать их actions.

Вот новый пример: простая комната с выключателем для переключения цвета фона — ой, я имею в виду света.



State хранится в store, который также имеет функцию переключения света. Как state, так и функция передаются через контекст.

import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";

// Пустой context.
const RoomContext = React.createContext();

// Компонент, задачей которого является управление 
// светом в комнате.
class RoomStore extends React.Component {
  state = {
    isLit: false
  };

  toggleLight = () => {
    this.setState(state => ({ isLit: !state.isLit }));
  };

  render() {
    // Передаем state и onToggleLight action
    return (
      <RoomContext.Provider
        value={{
          isLit: this.state.isLit,
          onToggleLight: this.toggleLight
        }}
      >
        {this.props.children}
      </RoomContext.Provider>
    );
  }
}

// Получаем информацию о том, светло ли в комнате, 
// и функцию для переключения света из RoomContext.
const Room = () => (
  <RoomContext.Consumer>
    {({ isLit, onToggleLight }) => (
      <div className={`room ${isLit ? "lit" : "dark"}`}>
        The room is {isLit ? "lit" : "dark"}.
        <br />
        <button onClick={onToggleLight}>Flip</button>
      </div>
    )}
  </RoomContext.Consumer>
);

const App = () => (
  <div className="app">
    <Room />
  </div>
);

// Оборачиваем все приложение в RoomStore,
// это будет работать так же хорошо как и в прошлом примере.
ReactDOM.render(
  <RoomStore>
    <App />
  </RoomStore>,
  document.querySelector("#root")
);

Вот полный рабочий пример в CodeSandbox.

Так все-таки, что использовать, Context или Redux?

Теперь, когда вы видели оба пути — какой из них стоит использовать? Я знаю, что вы хотите просто услышать ответ на данный вопрос, но я вынужден ответить — «зависит от вас».

Это зависит от того, насколько велико ваше приложение сейчас или насколько быстро будет расти. Сколько людей будет работать над ним — только вы или большая команда? Насколько опытны вы или ваша команда в работе с функциональными концепциями, на которые полагается Redux (такими как иммутабельность и чистые функции).

Пагубной ошибкой, пронизывающей всю экосистему JavaScript, является идея конкуренции. Есть идея, что каждый выбор — игра с нулевой суммой: если вы используете библиотеку A, вы не должны использовать ее конкурента библиотеку Б. Что когда выходит новая библиотека, которая чем-то лучше предыдущей, она должна вытеснить существующую. Что все должно быть или / или, что вы должны либо выбрать самое новое и лучшее, либо быть отодвинутым на задний план вместе с разработчиками из прошлого.

Лучший подход — посмотреть на этот замечательный выбор на примере, набора инструментов. Это похоже на выбор между использованием отвертки или мощного шуруповерта. Для 80% случаев шуруповерт выполнит работу легче и быстрее, чем отвертка. Но для других 20% отвертка будет лучшим выбором (мало места, или предмет тонкий). Когда я купил шуруповерт, я не сразу выбросил отвертку, он не заменил ее, а просто дал мне еще один вариант. Другой способ решить проблему.

Context не «заменяет» Redux, не более, чем React «заменил» Angular или jQuery. Черт, я все еще использую jQuery, когда мне нужно что-то сделать быстро. Я по-прежнему иногда использую серверные шаблоны EJS вместо того, чтобы развернуть приложение React. Иногда React — больше, чем вам нужно для выполнения задачи. То же самое касается и Redux.

Сегодня, если Redux — больше, чем вам нужно, вы можете использовать context.

Обучение React может быть тяжелым — так много библиотек и инструментов!

Мой совет? Игнорировать их все :)

Для пошагового обучения прочитайте мою книгу «Pure React».
Tags:
Hubs:
+15
Comments 24
Comments Comments 24

Articles