Skip to main content

Руководство: Использование connect API

Подсказка

На данный момент мы рекомендуем использовать React-Redux хуки. Как бы то ни было, connect по-прежнему работает.

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

Мы работаем над новым вводным туториалом про API хуков. А пока предлагаем прочитать Redux Fundamentals, Part 5: UI and React для руководства по хукам.

Чтобы увидеть как используется React Redux на практике, мы шаг за шагом разберём пример приложения со списком дел.

Пример приложения со списком дел

Переместиться

React компоненты для UI

Мы реализуем React компоненты интерфейса следующим образом:

  • TodoApp — начальный компоненты нашего приложения. Он отрисовывает заголовок и компоненты: AddTodo, TodoList, VisibilityFilters.
  • AddTodo — компонент, позволяющий пользователю создать задачу и добавить её к списку дел через кнопку “Add Todo”:
    • Компонент использует "контроллируемый" input, который обновляет своё состояние при вызове события onChange.
    • Когда пользователь нажимает на кнопку “Add Todo”, отправляется действие (которые мы предоставим с помощью React Redux) для добавления задачи в хранилище.
  • TodoList — компонент, отрисовывающий список задач:
    • При активации фильтра VisibilityFilters отрисовывается отфильтрованный список задач.
  • Todo — компонент, отображающий сущность задачи:
    • Отображает содержимое задачи и показывает её статус. Завершённая задача будет вычеркнута.
    • В нём отправляет действие при изменении статуса через событие onClick.
  • VisibilityFilters отрисовывает простой набор фильтров: all, completed и incomplete. Фильтрация будет происходить при нажатии:
    • Компонент принимает свойство activeFilter от его родителя, указывающее выбранный пользователем фильтр. Активный фильтр выделяется подчеркиванием.
    • Компонент отправляет действие setFilter при выборе фильтра.
  • constants содержит данные констант для нашего приложения.
  • И, наконец, index отрисовывает наше приложение в DOM дерево.

Redux хранилище(store)

Redux-часть приложения была настроена с использованием шаблона, рекомендованного в документации Redux:

  • Хранилище(store)
    • todos: нормализованный редюсер(reducer) задач. Он содержит в себе byIds — выборку из задач и allIds, содержащий список из id всех задач.
    • visibilityFilters: строка all, completed или incomplete.
  • Создатели Действий
    • addTodo создаёт действие для добавления задачи. Он принимает строку content и возвращает действие ADD_TODO с переменной payload, содержащей самоувеличивающийся id и content
    • toggleTodo создаёт действие для переключения статуса задачи. Принимает номер задачи id и возвращает действие TOGGLE_TODO с payload, содержащей только её id
    • setFilter создаёт действие для установки состояния активного фильтра. Принимает строку filter и возвращает действие SET_FILTER с payload, содержащей сам фильтр filter
  • Редюсеры(Reducers)
    • todos редюсер
      • Добавляет id к своему полю allIds и устанавливает задачу в своем поле byIds после получения действия ADD_TODO.
      • Переключает состояние поля completed для задачи, получая действие TOGGLE_TODO
    • visibilityFilters редюсер задаёт свой срез хранилища(state) для нового фильтра, который он получает из payload действия SET_FILTER
  • Типы действий (actions)
    • Мы используем файл actionTypes.js, который хранит константы для типов действий, чтобы их переиспользовать
  • Селекторы
    • getTodoList возвращает список allIds из хранилища todos
    • getTodoById находит задачу в хранилище по id
    • getTodos немножко более сложный. Он принимает в себя id из allIds, ищет каждую задачу из byIds и возвращает массив из задач
    • getTodosByVisibilityFilter фильтрует задачи согласно выбранному фильтру

Вы можете посмотреть код на CodeSandbox, там есть UI компоненты и описанное выше, но ещё не подключенное Redux хранилище(store).


Теперь мы покажем, как подключить хранилище(store) к приложению, используя React Redux.

Внедрение хранилища(store)

Сначала нам нужно сделать хранилище store доступным нашему приложению. Для этого мы оборачиваем наше приложение в компонент <Provider />, импортируемый из React Redux.

// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import TodoApp from './TodoApp'

import { Provider } from 'react-redux'
import store from './redux/store'

// React 18
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<Provider store={store}>
<TodoApp />
</Provider>
)

Обратите внимание, <TodoApp /> сейчас обёрнут в <Provider />, куда мы через пропсы передаём store.

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

React Redux предоставляет функцию connect для чтения из Redux хранилища (и повторного чтения при обновлении хранилища).

Функция connect принимает 2 не обязательных аргумента:

  • mapStateToProps: вызывается каждый раз, когда состояние хранилища(store) изменяется. Он получает всё содержимое хранилища(store) и возвращает объект с данными для этого компонента.
  • mapDispatchToProps: этот параметр может быть объектом или функцией.
    • Если это функция, она вызывается единожды при создании компонента. В аргументах она получает dispatch и должна возвращать объект с функциями, использующими dispatch для отправки действий в хранилище(store).
    • Если это объект, он должен содержать Создателей Действий, где каждый Создатель Действия будет превращен во вспомогательную функцию, которая автоматически отправляет своё действие при вызове. Обратите внимание: мы рекомендуем использовать форму “object shorthand”.

Обычно connect вызывается так:

const mapStateToProps = (state, ownProps) => ({
// ... вычисляемые данные из состояния и, опционально, ownProps
})

const mapDispatchToProps = {
// ... обычно это объект, полный Создателей Действий
}

// `connect` возвращает функцию, которая принимает компонент для обёртки:
const connectToStore = connect(mapStateToProps, mapDispatchToProps)
// Эта функция принимает наш компонент и возвращает подключенный к Redux хранилищу(store) компонент-обёртку:
const ConnectedComponent = connectToStore(Component)

// Обычно мы делаем и то, и другое за один шаг, например:
connect(mapStateToProps, mapDispatchToProps)(Component)

Давайте сперва поработаем над компонентом <AddTodo />. Ему будет необходимо вызывать изменения в хранилище store для создания новых задач. Следовательно, он должен уметь отправлять dispatch действия в хранилище(store).

Наш Cоздатель Действия addTodo выглядит так:

// redux/actions.js
import { ADD_TODO } from './actionTypes'

let nextTodoId = 0
export const addTodo = (content) => ({
type: ADD_TODO,
payload: {
id: ++nextTodoId,
content,
},
})

// ... другие действий

При передаче его в connect, наш компонент получит в пропсах функцию и при её вызове автоматически отправит действие в хранилище(store).

// components/AddTodo.js

// ... другие import
import { connect } from 'react-redux'
import { addTodo } from '../redux/actions'

class AddTodo extends React.Component {
// ... реализация компонента
}

export default connect(null, { addTodo })(AddTodo)

Обратите внимание, что <AddTodo /> обёрнут в родительский компонент <Connect(AddTodo) />. Тем временем, <AddTodo /> теперь получит 1 пропс: функцию addTodo для отправки действия ADD_TODO.

Нам также потребуется реализовать функцию handleAddTodo, которая вызовет действие addTodo и сбросит значение input

// components/AddTodo.js

import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../redux/actions'

class AddTodo extends React.Component {
// ...

handleAddTodo = () => {
// Отправляет действие для добавления задачи
this.props.addTodo(this.state.input)

// Приводит в изначальное состояние
this.setState({ input: '' })
}

render() {
return (
<div>
<input
onChange={(e) => this.updateInput(e.target.value)}
value={this.state.input}
/>
<button className="add-todo" onClick={this.handleAddTodo}>
Add Todo
</button>
</div>
)
}
}

export default connect(null, { addTodo })(AddTodo)

Теперь наш <AddTodo /> связан с хранилищем(store). При добавлении задачи компонент отправит действие для изменения хранилища(store). Пока что мы не видим этого в приложении, потому что остальные компоненты ещё не подключены. Если у вас установлено Redux DevTools Extension, вы сможете увидеть отправленное действие:

Вы также увидите изменение хранилища(store):

Компонент <TodoList /> ответственен за отрисовку списка задач. Следовательно, он будет читать данные задач из хранилища. Мы реализуем это, вызывая connect с параметром mapStateToProps — функцией, описывающей часть данных, которая нам нужна из хранилища(store).

Наш компонент <Todo /> принимает задачу как пропс. Мы получим эту информацию из поля byIds объекта todos. Нам также нужна информация из поля allIds, указывающего какие задачи и в каком порядке должны быть отрисованы. Наша функция mapStateToProps может выглядеть так:

// components/TodoList.js

// ...другие import
import { connect } from "react-redux";

const TodoList = // ... реализация UI компонента

const mapStateToProps = state => {
const { byIds, allIds } = state.todos || {};
const todos =
allIds && allIds.length
? allIds.map(id => (byIds ? { ...byIds[id], id } : null))
: null;
return { todos };
};

export default connect(mapStateToProps)(TodoList);

К счастью, для этого у нас уже есть селектор getTodos. Мы можем просто импортировать его и использовать здесь.

// redux/selectors.js

export const getTodosState = (store) => store.todos

export const getTodoList = (store) =>
getTodosState(store) ? getTodosState(store).allIds : []

export const getTodoById = (store, id) =>
getTodosState(store) ? { ...getTodosState(store).byIds[id], id } : {}

export const getTodos = (store) =>
getTodoList(store).map((id) => getTodoById(store, id))
// components/TodoList.js

// ...other imports
import { connect } from "react-redux";
import { getTodos } from "../redux/selectors";

const TodoList = // ... реализация UI компонента

export default connect(state => ({ todos: getTodos(state) }))(TodoList);

Мы рекомендуем инкапсулировать сложные вычисления или поиск в функции селекторы. Кроме того, вы можете дополнительно оптимизировать производительность, используя Reselect для написания мемоизированных(“memoized”) селекторов, которые пропустят необязательную работу. (Посмотрите страницу документации Redux: Вычисление производных данных и запись из блога Идиоматический Redux: использование Reselect селекторов для инкапсуляции и производительности для получения информации о том, как и зачем использовать селекторы.)

Теперь, когда наш компонент <TodoList /> подключен к хранилищу(store), он получит список задач и передаст каждую задачу в компонент <Todo />. <Todo /> в свою очередь отобразит их на экране. Теперь попытаемся добавить задачу. Она должна появиться в нашем списке задач!

Мы подключим больше компонентов. Перед этим, давайте остановимся и узнаем немного о connect.

Распространенные способы вызова connect

В зависимости от типа компонента, с которым вы работаете, существуют различные способы вызова connect , наиболее распространённые из них представлены в таблице:

Не подписываются на хранилище(store)Подписываются на хранилище(store)
Не включают Создателей Действийconnect()(Component)connect(mapStateToProps)(Component)
Включают Создателей Действийconnect(null, mapDispatchToProps)(Component)connect(mapStateToProps, mapDispatchToProps)(Component)

Компонент, не подписанный на хранилище(store) и без Создателей Действий

Если вы вызовете connect без аргументов, ваш компонент:

  • не перерисовывается при изменении хранилища(store)
  • получит props.dispatch, который вы можете использовать для отправки действий
// ... Реализация компонента Component
export default connect()(Component) // Компонент получит `dispatch` (Прямо как наш <TodoList />!)

Компонент, подписанный на хранилище(store), но без Создателей Действий

Если вы вызываете connect только с mapStateToProps, ваш компонент:

  • подписывается на значения из извлечённые mapStateToProps из хранилища(store) и будет перерисовываться только при изменении этих значений
  • получит props.dispatch, который вы можете использовать для отправки действий
// ... Реализация компонента Component
const mapStateToProps = (state) => state.partOfState
export default connect(mapStateToProps)(Component)

Компонент, не подписанный на хранилище(store), но включающий Создателей Действий

Если вы вызываете connect только с mapDispatchToProps, ваш компонент:

  • не перерисовывается при изменении хранилища(store)
  • получит как пропс каждый из Создателей Действий, который вы включите в mapDispatchToProps и при вызове автоматически отправит действия.
import { addTodo } from './actionCreators'
// ... Реализация компонента Component
export default connect(null, { addTodo })(Component)

Компонент, подписанный на хранилище(store) и включающий Создателей Действий

Если вы вызываете connect, передавая оба mapStateToProps и mapDispatchToProps, ваш компонент:

  • подписывается на значения из извлечённые mapStateToProps из хранилища(store) и будет перерисовываться только когда эти значения меняются
  • получит как пропс каждый из Создателей Действий, который вы включите в mapDispatchToProps и при вызове автоматически отправит действия.
import * as actionCreators from './actionCreators'
// ... Реализация компонента Component
const mapStateToProps = (state) => state.partOfState
export default connect(mapStateToProps, actionCreators)(Component)

Эти 4 случая покрывают основные пути использования connect. Чтобы узнать больше о connect, прочитайте описание API, которое объяснит это более подробно.


Теперь, давайте подключим остальную часть приложения <TodoApp />.

Как нам следует реализовать взаимодействие с переключением статуса задач? Внимательный читатель уже может дать ответ. Если вы настроили своё окружение и дошли до этого момента, самое время оставить статью в стороне и реализовать эту функцию самостоятельно. Не будет ничего удивительного в том, как мы подключим компонент <Todo />, чтобы он отправлял toggleTodo:

// components/Todo.js

// ... другие import
import { connect } from "react-redux";
import { toggleTodo } from "../redux/actions";

const Todo = // ... реализация компонента

export default connect(
null,
{ toggleTodo }
)(Todo);

Теперь можно изменить статус задачи. Мы почти закончили!

Наконец, давайте реализуем VisibilityFilters

Компонент <VisibilityFilters /> должен доставать из хранилища(store) активный фильтр и отправлять действия в хранилище(store). Следовательно, нам нужно реализовать оба mapStateToProps и mapDispatchToProps. mapStateToProps может просто получать состояние visibilityFilter. И mapDispatchToProps будет содержать в себе Создатель Действия setFilter.

// components/VisibilityFilters.js

// ... другие import
import { connect } from "react-redux";
import { setFilter } from "../redux/actions";

const VisibilityFilters = // ... Реализация компонента

const mapStateToProps = state => {
return { activeFilter: state.visibilityFilter };
};
export default connect(
mapStateToProps,
{ setFilter }
)(VisibilityFilters);

Тем временем, нам необходимо обновить наш компонент <TodoList /> для фильтрации задач в соответствии с активным фильтром. Ранее, вызов mapStateToProps, который мы передали в функцию connect для <TodoList />, был простым селектором, который возвращал весь список задач. Давайте напишем еще один селектор, который поможет фильтровать задачи по их статусу.

// redux/selectors.js

// ... другие селекторы
export const getTodosByVisibilityFilter = (store, visibilityFilter) => {
const allTodos = getTodos(store)
switch (visibilityFilter) {
case VISIBILITY_FILTERS.COMPLETED:
return allTodos.filter((todo) => todo.completed)
case VISIBILITY_FILTERS.INCOMPLETE:
return allTodos.filter((todo) => !todo.completed)
case VISIBILITY_FILTERS.ALL:
default:
return allTodos
}
}

И подключаем к хранилищу(store) с помощью селектора:

// components/TodoList.js

// ...

const mapStateToProps = (state) => {
const { visibilityFilter } = state
const todos = getTodosByVisibilityFilter(state, visibilityFilter)
return { todos }
}

export default connect(mapStateToProps)(TodoList)

Теперь мы закончили очень простой пример приложения списка задач с React Redux. Все наши компоненты подключены! Разве это не прекрасно? 🎉🎊

Ссылки

Помощь