Приложение React — это, по сути, дерево компонентов, которые обмениваются данными друг с другом. Передача данных между компонентами обычно безболезненна. Однако по мере роста дерева приложений становится все труднее передавать данные, сохраняя удобочитаемую кодовую базу.
Предположим, у нас есть следующая древовидная структура:
Вот простое дерево с 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. Это означает две вещи:
- Нам нужно реализовать прослушиватель событий onChange и хранить его в Store независимо от того, что вводит пользователь.
- Нам нужно сохранить свойство 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!