1. Code
  2. JavaScript
  3. React

Начало работы с Redux: подключение Redux к React

Scroll to top

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().

К концу этой статьи вы узнаете

  1. какая разница между контейнерными компонентами и презентационными компонентами
  2. о библиотеке react-redux
  3. Как связать react и redux с помощью connect()
  4. как отправлять действия, используя mapDispatchToProps
  5. как получить состояние, используя mapStateToProps

Код для учебника доступен на GitHub в react-redux-demo. Возьмите код из ветки v2 и используйте его в качестве отправной точки для этого урока. Если вам интересно узнать, как приложение выглядит к концу этого урока, попробуйте ветку v3. Давайте начнем.

Проектирование иерархии компонентов: умный компонент против тупого

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

Говорят, что компоненты представления глупы, потому что они обеспокоены тем, как все выглядит. Они отделены от бизнес-логики приложения и получают данные и обратные вызовы от родительского компонента исключительно через props. Им все равно, подключено ли ваше приложение к хранилищу Redux, если данные поступают из локального состояния родительского компонента.

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

Я подробно рассмотрел эту тему в другом учебном пособии «Компоненты с состоянием и без состояния в React».

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

Designing component HierarchyDesigning component HierarchyDesigning component Hierarchy

Презентационные компоненты

Вот презентационные компоненты, которые мы будем использовать в этом уроке.

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 для:

  1. того чтобы подписаться на хранилище и сопоставить его состояние с вашими props
  2. действия по отправке и сопоставление обратных вызовов в ваши 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 и начнем рассылать действия, связанные с извлечением данных с сервера. Поделитесь своими мыслями в комментариях!