Начало работы с Redux: подключение Redux к React
Russian (Pусский) translation by Ilya Nikov (you can also view the original English article)
Это третья часть серии статей о начале работы с Redux, и в этом уроке мы узнаем, как подключить хранилище Redux к React. Redux - это независимая библиотека, которая работает со всеми популярными интерфейсными библиотеками и фреймворками. И это работает безупречно с React из-за его функционального подхода.
Вам не нужно следовать предыдущим частям этой серии, чтобы этот урок имел смысл. Если вы хотите узнать, как использовать React с Redux, вы можете воспользоваться кратким обзором, приведенным ниже, а затем взять код из предыдущей части и начать с него.
Быстрый обзор
В первом посте мы узнали о рабочем процессе Redux и ответили на вопрос, зачем нужен Redux?. Мы создали очень простое демонстрационное приложение и показали, как связаны различные компоненты Redux - действия, редукторы и хранилище.
В предыдущем посте мы начали создавать приложение со списком контактов, которое позволяет добавлять контакты, а затем отображать их в виде списка. Для нашего списка контактов был создан store Redux, и мы добавили несколько редукторов и действий. Мы попытались отправить действия и получить новое состояние, используя методы store, такие как store.dispatch() и store.getState().
К концу этой статьи вы узнаете
- какая разница между контейнерными компонентами и презентационными компонентами
- о библиотеке react-redux
- Как связать react и redux с помощью
connect() - как отправлять действия, используя
mapDispatchToProps
- как получить состояние, используя
mapStateToProps
Код для учебника доступен на GitHub в react-redux-demo. Возьмите код из ветки v2 и используйте его в качестве отправной точки для этого урока. Если вам интересно узнать, как приложение выглядит к концу этого урока, попробуйте ветку v3. Давайте начнем.
Проектирование иерархии компонентов: умный компонент против тупого
Это концепция, о которой вы, вероятно, слышали раньше. Но давайте кратко рассмотрим разницу между умными и глупыми компонентами. Напомним, что мы создали два отдельных каталога для компонентов, один из которых назвал containers/, а другой components/. Преимущество этого подхода заключается в том, что логика поведения отделена от представления.
Говорят, что компоненты представления глупы, потому что они обеспокоены тем, как все выглядит. Они отделены от бизнес-логики приложения и получают данные и обратные вызовы от родительского компонента исключительно через props. Им все равно, подключено ли ваше приложение к хранилищу Redux, если данные поступают из локального состояния родительского компонента.
Компоненты контейнера, с другой стороны, имеют дело с поведенческой частью и должны содержать очень ограниченную разметку и стиль DOM. Они передают данные, которые должны быть обработаны, для глупых компонентов в качестве props.
Я подробно рассмотрел эту тему в другом учебном пособии «Компоненты с состоянием и без состояния в React».
Двигаясь дальше, давайте посмотрим, как мы собираемся организовать наши компоненты.



Презентационные компоненты
Вот презентационные компоненты, которые мы будем использовать в этом уроке.
components/AddContactForm.jsx
1 |
import React from 'react'; |
2 |
|
3 |
const AddContactForm = ({onInputChange, onFormSubmit}) =>
|
4 |
( |
5 |
<form> |
6 |
<div className="form-group"> |
7 |
<label htmlFor="emailAddress">Email address</label> |
8 |
<input type="email" class="form-control" name="email" onChange={onInputChange} placeholder="name@example.com" />
|
9 |
</div> |
10 |
|
11 |
{/* Some code omitted for brevity */}
|
12 |
|
13 |
<div className="form-group"> |
14 |
<label htmlFor="physicalAddress">Address</label> |
15 |
<textarea className="form-control" name="address" onChange={onInputChange} rows="3"></textarea>
|
16 |
</div> |
17 |
|
18 |
<button type="submit" onClick={onFormSubmit} class="btn btn-primary"> Submit </button>
|
19 |
</form> |
20 |
) |
21 |
|
22 |
export default AddContactForm; |
Это HTML-форма для добавления нового контакта. Компонент получает обратные вызовы onInputChange и onFormSubmit в качестве props. Событие onInputChange запускается при изменении входного значения и onFormSubmit при отправке формы.
components/ContactList.jsx
1 |
const ContactList = (props) => { |
2 |
return( <ul className="list-group" id="contact-list"> |
3 |
{props.contactList.map( |
4 |
(contact) => |
5 |
<li key={contact.email} className="list-group-item"> |
6 |
<ContactCard contact = {contact}/> |
7 |
</li> |
8 |
)}
|
9 |
</ul>) |
10 |
}
|
11 |
|
12 |
export default ContactList; |
Этот компонент получает массив контактных объектов в качестве props и, следовательно, имя ContactList. Мы используем метод Array.map(), чтобы извлечь отдельные контактные данные, а затем передать эти данные в <ContactCard/>.
components/ContactCard.jsx
1 |
const ContactCard = ({contact}) => {
|
2 |
|
3 |
return( |
4 |
<div> |
5 |
<div className="col-xs-4 col-sm-3"> |
6 |
{contact.photo !== undefined ? <img src={contact.photo} alt={contact.name} className="img-fluid rounded-circle" /> :
|
7 |
<img src="img/profile_img.png" alt ={contact.name} className="img-fluid rounded-circle" />}
|
8 |
</div> |
9 |
<div className="col-xs-8 col-sm-9"> |
10 |
<span className="name">{contact.name + ' ' + contact.surname}</span><br/>
|
11 |
|
12 |
{/* Some code omitted for brevity */}
|
13 |
|
14 |
</div> |
15 |
</div> |
16 |
|
17 |
) |
18 |
} |
19 |
|
20 |
export default ContactCard; |
Этот компонент получает объект контакта и отображает имя и изображение контакта. Для практических приложений может иметь смысл размещать изображения JavaScript в облаке.
Компоненты контейнера
Мы также собираемся создать компоненты контейнера barebones.
containers/Contacts.jsx
1 |
class Contacts extends Component { |
2 |
|
3 |
constructor(props) { |
4 |
super(props); |
5 |
this.returnContactList = this.returnContactList.bind(this); |
6 |
}
|
7 |
returnContactList() { |
8 |
// Retrieve contactlist from the store
|
9 |
}
|
10 |
|
11 |
render() { |
12 |
|
13 |
return ( |
14 |
<div> |
15 |
|
16 |
<AddContact/> |
17 |
<br /> |
18 |
<ContactList contactList= {this.returnContactList()} /> |
19 |
</div> |
20 |
);
|
21 |
}
|
22 |
}
|
23 |
|
24 |
|
25 |
export default Contacts; |
Функция returnContactList() извлекает массив объектов контактов и передает его компоненту ContactList. Поскольку returnContactList() извлекает данные из хранилища, мы пока оставим эту логику пустой.
containers/AddContacts.jsx
1 |
class AddContact extends Component {
|
2 |
constructor(props) {
|
3 |
super(props); |
4 |
|
5 |
/* Function binding goes here. Omitted for brevity */ |
6 |
} |
7 |
|
8 |
showAddContactBox() {
|
9 |
/* Logic for toggling ContactForm */ |
10 |
} |
11 |
|
12 |
handleInputChange(event) {
|
13 |
const target = event.target; |
14 |
const value = target.value; |
15 |
const name = target.name; |
16 |
|
17 |
/* Logic for handling Input Change */ |
18 |
|
19 |
} |
20 |
|
21 |
handleSubmit(e) {
|
22 |
e.preventDefault(); |
23 |
|
24 |
/* Logic for hiding the form and update the state */ |
25 |
} |
26 |
|
27 |
/* Renders the AddContactForm */ |
28 |
renderForm() {
|
29 |
return( |
30 |
<div className="col-sm-8 offset-sm-2"> |
31 |
<AddContactForm onFormSubmit={this.handleSubmit} onInputChange={this.handleInputChange} />
|
32 |
</div> |
33 |
) |
34 |
} |
35 |
render() {
|
36 |
return( |
37 |
<div> |
38 |
|
39 |
{ /* A conditional statement goes here that checks whether the form
|
40 |
should be displayed or not */} |
41 |
</div> |
42 |
) |
43 |
} |
44 |
} |
45 |
|
46 |
|
47 |
export default AddContact; |
48 |
Мы создали три метода-обработчика, которые соответствуют трем действиям. Все они отправляют действия для обновления состояния. В методе рендеринга мы исключили логику для отображения/скрытия формы, потому что нам нужно получить состояние.
Теперь давайте посмотрим, как связать react и redux вместе
Библиотека react-redux
Привязки React по умолчанию недоступны в Redux. Сначала вам нужно будет установить дополнительную библиотеку react-redux.
1 |
npm install --save react-redux |
Библиотека экспортирует только два API, которые вам нужно запомнить, компонент <Provider/> и функцию более высокого порядка, известную как connect().
Компонент провайдера
Такие библиотеки, как Redux, должны сделать доступными данные хранилища для всего дерева компонентов React, начиная с корневого компонента. Шаблон Provider позволяет библиотеке передавать данные сверху вниз. Приведенный ниже код демонстрирует, как поставщик волшебным образом добавляет состояние ко всем компонентам в дереве компонентов.
Демонстрационный код
1 |
import { Provider } from 'react-redux' |
2 |
|
3 |
ReactDOM.render( |
4 |
<Provider store={store}> |
5 |
<App /> |
6 |
</Provider>, |
7 |
document.getElementById('root') |
8 |
)
|
Все приложение должно иметь доступ к хранилищу. Итак, мы оборачиваем провайдера вокруг компонента приложения и затем добавляем необходимые данные в контекст дерева. Потомки компонента получают доступ к данным.
Метод connect()
Теперь, когда мы предоставили хранилище нашему приложению, нам нужно подключить React к нему. Единственный способ связаться с хранилищем - отправлять действия и получать данные о состоянии. Ранее мы использовали store.dispatch() для отправки действий и store.getState() для получения последнего снимка состояния. Функция connect() позволяет вам сделать это, но с помощью двух методов, известных как mapDispatchToProps и mapStateToProps. Я продемонстрировал эту концепцию в следующем примере:
Демонстрационный код
1 |
import {connect} from 'react-redux' |
2 |
|
3 |
const AddContact = ({newContact, addContact}) => { |
4 |
return ( |
5 |
<div> |
6 |
{newContact.name} <br /> |
7 |
{newContact.email} <br /> |
8 |
{newContact.phone} <br /> |
9 |
|
10 |
Are you sure you want to add this contact? |
11 |
<span onClick={addContact}> Yes </span> |
12 |
</div> |
13 |
)
|
14 |
}
|
15 |
|
16 |
const mapStateToProps = state => { |
17 |
return { |
18 |
newContact : state.contacts.newContact |
19 |
}
|
20 |
}
|
21 |
|
22 |
const mapDispatchToProps = dispatch => { |
23 |
return { |
24 |
addContact : () => dispatch(addContact()) |
25 |
}
|
26 |
}
|
27 |
|
28 |
export default connect( |
29 |
mapStateToProps, |
30 |
mapDispatchToProps
|
31 |
)(AddContact) |
mapStateToProps и mapDispatchToProps оба возвращают объект, и ключ этого объекта становится prop подключенного компонента. Например, state.contacts.newContact сопоставляется с props.newContact. Создатель действия addContact() сопоставлен с props.addContact.
Но чтобы это работало, вам нужна последняя строка в фрагменте кода выше.
1 |
export default connect( |
2 |
mapStateToProps, |
3 |
mapDispatchToProps
|
4 |
)(AddContact) |
Вместо того, чтобы экспортировать компонент AddContact напрямую, мы экспортируем связанный компонент. Соединение предоставляет addContact и newContact в качестве реквизита для компонента <AddContact/>.
Как соединить React и Redux?
Далее мы рассмотрим шаги, которые необходимо выполнить для подключения React и Redux.
Установка библиотеки react-redux
Установите библиотеку react-redux, если вы еще этого не сделали. Вы можете использовать NPM или yarn, чтобы установить ее.
1 |
npm install react-redux --save |
Предоставьте хранилище вашему компоненту приложения
Сначала создайте хранилище. Затем сделайте объект хранилища доступным для вашего дерева компонентов, передав его в качестве prop <Provider/>.
index.js
1 |
import React from 'react'; |
2 |
import {render}from 'react-dom'; |
3 |
import { Provider } from 'react-redux' |
4 |
import App from './App'; |
5 |
|
6 |
import configureStore from './store' |
7 |
|
8 |
const store = configureStore(); |
9 |
render( |
10 |
<Provider store={store}> |
11 |
<App /> |
12 |
</Provider>, |
13 |
document.getElementById('root') |
14 |
)
|
Подключите контейнеры React к Redux
Функция connect используется для привязки контейнера React к Redux. Это означает, что вы можете использовать функцию connect для:
- того чтобы подписаться на хранилище и сопоставить его состояние с вашими props
- действия по отправке и сопоставление обратных вызовов в ваши props
После подключения вашего приложения к Redux вы можете использовать this.props для доступа к текущему состоянию, а также для отправки действий. Я собираюсь продемонстрировать процесс на компоненте AddContact. AddContact должен отправить три действия и получить состояние двух свойств из хранилища. Давайте посмотрим на код.
Сначала импортируйте connect в AddContact.jsx.
1 |
import { connect } from 'react-redux'; |
Во-вторых, создайте два метода mapStateToProps и mapDispatchToProps.
1 |
function mapStateToProps(state) { |
2 |
return { |
3 |
|
4 |
isHidden : state.ui.isAddContactFormHidden, |
5 |
newContact: state.contacts.newContact |
6 |
}
|
7 |
}
|
8 |
|
9 |
function mapDispatchToProps(dispatch) { |
10 |
return { |
11 |
onFormSubmit: (newContact) => { |
12 |
dispatch(addContact(newContact)); |
13 |
},
|
14 |
onInputChange: (name,value) => { |
15 |
|
16 |
dispatch(handleInputChange(name,value)); |
17 |
},
|
18 |
|
19 |
onToggle: () => { |
20 |
dispatch(toggleContactForm()); |
21 |
}
|
22 |
}
|
23 |
}
|
mapStateToProps получает состояние хранилища в качестве аргумента. Он возвращает объект, который описывает, как состояние магазина отображается в ваших реквизитах. mapDispatchToProps возвращает похожий объект, который описывает, как действия по отправке отображаются на ваши реквизиты.
Наконец, мы используем connect для связывания компонента AddContact с двумя функциями следующим образом:
1 |
export default connect(mapStateToProps, mapDispatchToProps) (AddContact) |
Обновите компоненты контейнера для использования реквизита
Теперь реквизиты компонента оборудованы для чтения состояния из хранилища и отправки действий. Логика для handleInputChange, handleSubmit и showAddContactBox должна быть обновлена следующим образом:
1 |
showAddContactBox() { |
2 |
const { onToggle } = this.props; |
3 |
onToggle(); |
4 |
}
|
5 |
|
6 |
handleInputChange(event) { |
7 |
const target = event.target; |
8 |
const value = target.value; |
9 |
const name = target.name; |
10 |
|
11 |
const { onInputChange } = this.props; |
12 |
onInputChange(name,value); |
13 |
}
|
14 |
|
15 |
handleSubmit(e) { |
16 |
e.preventDefault(); |
17 |
this.props.onToggle(); |
18 |
this.props.onFormSubmit(); |
19 |
}
|
Мы определили методы обработчика. Но все еще отсутствует одна часть - условный оператор внутри функции render.
1 |
render() { |
2 |
return( |
3 |
<div> |
4 |
{ this.props.isHidden === false ? this.renderForm(): <button onClick={this.showAddContactBox} className="btn"> Add Contact </button>} |
5 |
</div> |
6 |
)
|
7 |
}
|
Если isHidden имеет значение false, форма отображается. В противном случае отображается кнопка.
Отображение контактов
Мы завершили самую сложную часть. Теперь осталось только отобразить эти контакты в виде списка. Контейнер Contacts - лучшее место для этой логики.
1 |
import React, { Component } from 'react'; |
2 |
import { connect } from 'react-redux'; |
3 |
/* Component import omitted for brevity */
|
4 |
|
5 |
class Contact extends Component { |
6 |
|
7 |
constructor(props) { |
8 |
super(props); |
9 |
this.returnContactList = this.returnContactList.bind(this); |
10 |
}
|
11 |
|
12 |
returnContactList() { |
13 |
return this.props.contactList; |
14 |
}
|
15 |
|
16 |
|
17 |
render() { |
18 |
|
19 |
|
20 |
return ( |
21 |
<div> |
22 |
<br /> |
23 |
<AddContact/> |
24 |
<br /> |
25 |
<ContactList contactList= {this.returnContactList()} /> |
26 |
</div> |
27 |
);
|
28 |
}
|
29 |
}
|
30 |
|
31 |
function mapStateToProps(state) { |
32 |
return { |
33 |
contactList : state.contacts.contactList, |
34 |
|
35 |
}
|
36 |
}
|
37 |
|
38 |
|
39 |
export default connect(mapStateToProps, null) (Contact); |
40 |
Мы прошли ту же процедуру, что и выше, чтобы соединить компонент Contacts с хранилищем Redux. Функция mapStateToProps сопоставляет объект хранилища с props contactList. Затем мы используем connect для привязки значения prop к компоненту Contact. Второй аргумент для подключения является нулевым, потому что у нас нет никаких действий для отправки. Это завершает интеграцию нашего приложения с состоянием хранилища Redux.
Что дальше?
В следующем посте мы более подробно рассмотрим middleware и начнем рассылать действия, связанные с извлечением данных с сервера. Поделитесь своими мыслями в комментариях!





