Руководство: Использование 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) для добавления задачи в хранилище.
- Компонент использует "контроллируемый" input, который обновляет своё состояние при вызове события
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. Все наши компоненты подключены! Разве это не прекрасно? 🎉🎊
Ссылки
- Использование Redux с React
- Использование связки React Redux
- Подробно о компонентах высшего порядка
- Вычисление производных данных
- Идиоматический Redux: Используем селекторы Reselect для инкапсуляции и производительности
Помощь
- Reactiflux Redux канал
- StackOverflow
- GitHub Issues