Вам не обязательно использовать Redux: сравнение реквизита, Redux и React Context API

React.js Redux

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

Предположим, у нас есть следующая древовидная структура:

tree of components

Вот простое дерево с 3 уровнями. В этом дереве и узел D, и узел E обрабатывают некоторые похожие данные:Предположим, пользователь вводит некоторый текст в узле D, и мы хотим, чтобы этот текст отображался в узле E.

Как мы передаем данные от узла D к узлу E?

В этой статье описаны три возможных способа решения этой проблемы:

  • Опора дрель
  • Redux
  • React's context API

Цель этой статьи — сравнить эти подходы и показать, что при решении общей проблемы, такой как та, о которой мы только что говорили, можно придерживаться контекстного API React.

Метод 1: Сверление с опорой

Способ сделать это состоит в том, чтобы наивно передавать данные от ребенка к родителю и от родителя к ребенку через реквизиты, такие как:D->B->A, затем A->C->E.

Идея здесь состоит в том, чтобы использоватьonUserInputФункция запускается от дочернего узла к родительскому узлу для передачи входных данных от узла D к узлу A.stateсостоянии, мы затем отправляем данные из узла AstateСтатус передается узлу E.

Начнем с узла D:

class NodeD extends Component {
  render() {
    return (
      <div className="Child element">
        <center> D </center>
        <textarea
          type="text"
          value={this.props.inputValue}
          onChange={e => this.props.onUserInput(e.target.value)}
        />
      </div>
    );
  }
}

Когда пользователь что-то вводит,onChangeФункция прослушивателя начнется сpropизonUserInputЗапустите функцию и передайте пользовательский ввод. Эта функция в узле D активирует узел B.propсерединаonUserInputДругая функция, как показано ниже:

class NodeB extends Component {
  render() {
    return (
      <div className="Tree element">
        <center> B</center>
        <NodeD onUserInput={inputValue => this.props.onUserInput(inputValue)} />
      </div>
    );
  }
}

Наконец, достигнув корневого узла A,onUserInputЗапуск в узле B изменит состояние в узле A на значение, введенное пользователем.

class NodeA extends Component {
  state = {
    inputValue: ""
  };

  render() {
    return (
      <div className="Root element">
        <center> A </center>
        <NodeB
          onUserInput={inputValue => this.setState({ inputValue: inputValue })}
        />
        <NodeC inputValue={this.state.inputValue} />
      </div>
    );
  }
}

inputValueЗначения будут передаваться от узла C к дочернему узлу E через реквизиты:

class NodeE extends Component {
  render() {
    return (
      <div className="Child element">
        <center> E </center>
        {this.props.inputValue}
      </div>
    );
  }
}

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

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

Способ 2: использование Redux

Другой способ — использовать библиотеку управления состоянием, например Redux.

Redux is a predictable state container for JavaScript apps. The state of our whole application is stored in an object tree within a single store, which your app components depend on. Every component is connected directly to the global store, and the global store life cycle is independent of the components' life cycle.

Начнем с определения состояния приложения: данные, которые нас интересуют, это то, что пользователь введен в узле D. Мы хотим предоставить эти данные узлу E. Для этого мы можем предоставить эти данные в магазине. Узел E может затем подписаться на него, чтобы получить доступ к данным.

Вернемся немного в магазин.

Шаг 1: Определите Редуктор

Далее нужно определить наш редуктор. Наш редьюсер указывает, как изменяется состояние приложения в ответ на действия, переданные в хранилище.

Определенный редуктор выглядит следующим образом:

const initialState = {
  inputValue: ""
};

const reducer = (state = initialState, action) => {
  if (action.type === "USER_INPUT") {
    return {
      inputValue: action.inputValue
    };
  }
  return state;
};

Прежде чем пользователь что-либо введет, мы знаем, что наши данные состояния или inputValue будут пустой строкой. Поэтому мы используем пустую строкуinputValueдляreducerОпределяет начальное состояние по умолчанию.

Логика здесь такова: как только пользователь вводит что-то в узле D, мы «запускаем» или отправляем действие для обновления состояния, независимо от того, что ввел пользователь. «Обновление» здесь не означает «мутирование» или изменение текущего состояния, это означает возвратновое состояние.

Оператор if сопоставляет отправленное действие с новым состоянием для возврата в зависимости от его типа. Итак, мы уже знаем, что отправленное действие — это объект с ключом типа. Как мы можем получить введенное пользователем значение для нового состояния? Мы просто добавляем другое имя к объекту действия с именемinputValueключ , в нашем блоке редуктора мы делаем inputValue нового состояния таким входным значениемaction.inputValue. Таким образом, поведение нашего приложения будет следовать этой архитектуре:

{ type: "SOME_TYPE", inputValue: "some_value" }

В конечном итоге наш оператор отправки будет выглядеть так:

dispatch({ type: "SOME_TYPE", inputValue: "some_value" })

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

Хорошо, теперь мы знаем, как работает приложение: в нашем входном узле D мы отправляем действие типа USER_INPUT и передаем любое значение, которое только что ввел пользователь, а в нашем отображаемом узле E мы передаем текущее значение. состояние приложения.

Шаг 2. Определите магазин

Чтобы сделать наш магазин доступным, мы передаем его изreact-reduxимпортProviderкомпонент, затем поместитеAppзавернутый в него. Поскольку мы знаем, что узлы D и E будут использовать данные из этого хранилища, мы хотим, чтобы компонент Provider содержал общий родитель этих узлов, то есть либо корневой узел A, либо весь компонент приложения. давайте выберемAppкомпоненты входят в нашуProviderВ компоненте:

import reducer from "./store/reducer";
import { createStore } from "redux";
import { Provider } from "react-redux";

const store = createStore(reducer);
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

В настоящее время мы настроили хранилище и редюсер, а затем будем делать что-то в узле D и узле E.

Шаг 3: Реализуйте логику пользовательского ввода

Сначала смотрим на узел D, нас интересует, что пользователь ввел в элемент textarea. Это означает две вещи:

  1. Нам нужно реализовать прослушиватель событий onChange и хранить его в Store независимо от того, что вводит пользователь.
  2. Нам нужно сохранить свойство value в хранилище как текстовое поле.

Но прежде чем делать что-либо из этого, нам нужно настроить несколько вещей:

Сначала нам нужно подключить компонент Node D к нашему магазину. Для этого мы используем функцию connect() из react-redux. Он предоставляет подключенным компонентам данные, необходимые хранилищу, а также функции, которые можно использовать для отправки операций в хранилище.

This is why we use the two mapStateToProps and mapDispatchToProps which deal with the store's state and dispatch respectively. We want our node D component to be subscribed to our store updates, as in, our app's state updates. This means that any time the app's state is updated, mapStateToProps will be called. The results of mapStateToProps is an object which will be merged into our node D's component props. Our mapDispatchToProps function lets us create functions that dispatch when called, and pass those functions as props to our component. We will make use of this by returning new function that calls dispatch() which passes in an action.

В нашем примере дляmapStateToPropsфункция, мы толькоinputValueинтересно, поэтому мы возвращаем объект{ inputValue: state.inputValue }. дляmapDispatchToPropsмы возвращаем функциюonUserInput, который принимает входное значение в качестве аргумента и используетUSER_INPUTВведите действие отправки. возвращен новый объект состоянияmapStateToPropsа такжеonUserInputфункция для включения в наш компонентpropsсередина. Итак, мы определяем наш компонент:

class NodeD extends Component {
  render() {
    return (
      <div className="Child element">
        <center> D </center>
        <textarea
          type="text"
          value={this.props.inputValue}
          onChange={e => this.props.onUserInput(e.target.value)}
        />
      </div>
    );
  }
}
const mapStateToProps = state => {
  return {
    inputValue: state.inputValue
  };
};

const mapDispatchToProps = dispatch => {
  return {
    onUserInput: inputValue =>
      dispatch({ type: "USER_INPUT", inputValue: inputValue })
  };
};
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(NodeD);

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

Шаг 4: Реализуйте логику пользовательского вывода

Мы хотим отображать данные ввода пользователя на этом узле. Мы уже знаем, что эти данные в основном представляют собой текущее состояние нашего приложения, как и нашего магазина. Итак, в конечном итоге мы хотим получить доступ к этому хранилищу и отобразить его данные. Для этого нам сначала нужно подписать компонент Node E на обновления в хранилище, используя ту же функцию connect(), которую мы использовали ранее с mapStateToProps. После этого нам просто нужно использоватьthis.props.valДоступ к данным в хранилище из реквизита компонента:

class NodeE extends Component {
  render() {
    return (
      <div className="Child element">
        <center> E </center>
        {this.props.val}
      </div>
    );
  }
}
const mapStateToProps = state => {
  return {
    val: state.inputValue
  };
};

export default connect(mapStateToProps)(NodeE);

Наконец-то мы закончили с Redux! 🎉 Вы можете проверить нас прямо здесьсделал что-то.

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

...... У кого есть время?

Способ 3: используйте контекстный API React

Теперь давайте повторим тот же пример, используя контекстный API.

React Context API существует уже некоторое время, но только в версии React 16.3.0 его стало безопасно использовать в производственной среде. Логика здесь близка к Redux: у нас есть объект контекста, который содержит некоторые глобальные данные, к которым мы хотим получить доступ из других компонентов.

Сначала мы создаем объект контекста, который содержит начальное состояние приложения в качестве состояния по умолчанию, а затем мы создаем объект контекста.Providerс однимConsumerКомпоненты:

const initialState = {
  inputValue: ""
};

const Context = React.createContext(initialState);

export const Provider = Context.Provider;
export const Consumer = Context.Consumer;

нашProviderКомпонент имеет в качестве потомков все компоненты, из которых мы хотим получить доступ к данным контекста. подобноProviderнадReduxта же версия. Чтобы извлечь или манипулировать контекстом, мы используемConsumerЭквивалент компонентов.

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

В нашем примере мы будем использовать this.state.inputValue, а также функции, управляющие состоянием, например, нашу функцию onUserInput.

class App extends React.Component {
  state = {
    inputValue: ""
  };

  onUserInput = newVal => {
    this.setState({ inputValue: newVal });
  };

  render() {
    return (
      <Provider
        value={{ val: this.state.inputValue, onUserInput: this.onUserInput }}
      >
        <div className="App">
          <NodeA />
        </div>
      </Provider>
    );
  }
}

Теперь мы можем продолжать использовать компонент Consumer для доступа к данным компонента Provider :)

Для узла D, где пользователь вводит данные:

const NodeD = () => {
  return (
    <div className="Child element">
      <center> D </center>
      <Consumer>
        {({ val, onUserInput }) => (
          <textarea
            type="text"
            value={val}
            onChange={e => onUserInput(e.target.value)}
          />
        )}
      </Consumer>
    </div>
  );
};

Для узла E, где мы отображаем пользовательский ввод:

const NodeE = () => {
  return (
    <div className="Child element ">
      <center> E </center>
      <Consumer>{context => <p>{context.val}</p>}</Consumer>
    </div>
  );
};

Мы закончили с контекстной API-версией примера! 🎉 Не сложно? существуетздесьПроверить

Что, если мы хотим иметь доступ к большему количеству компонентов контекста? Мы можем обернуть их компонентом Provider и использовать компонент Consumer для доступа/манипулирования контекстом! Простой :)

хорошо, но какой из них я должен использовать

Мы видим, что наша версия примера Redux заняла больше времени, чем версия Context. Мы уже можем видеть Redux:

  • Требует больше строк кода и может быть слишком шаблонным с более сложными примерами (несколько компонентов для доступа к хранилищу).
  • Добавленная сложность: при работе со многими компонентами может быть разумнее разделить редьюсеры и типы действий из компонентов в уникальные папки/файлы.
  • Знакомство с кривой обучения: Некоторым разработчикам трудно самостоятельно изучить Redux, потому что это требует от вас изучения некоторых новых концепций: редьюсеры, диспетчеры, действия, преобразователи, промежуточное ПО...

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

Однако, если вы просто хотите сделать некоторые данные Global, чтобы получить доступ к нему из куча компонентов, вы можете увидеть из нашего примера, что API API redux и Rection делает примерно то же самое. Так что в некотором смысле, вам не нужно использовать redux!