Использование с React
С самого начала необходимо подчеркнуть, что Redux не имеет отношения к React. Вы можете писать приложения Redux с React, Angular, Ember, jQuery или обычным JavaScript.
Тем не менее, Redux особенно хорошо работает с такими библиотеками, как React и Deku, потому что они позволяют описывать пользовательский интерфейс как функцию состояния, а Redux отправляет обновления состояния в ответ на действия.
Мы будем использовать React для создания нашего простого приложения для задач.
Установка React Redux
Связующие элементы React по умолчанию не включены в Redux. Вам нужно установить их явно:
npm install --save react-redux
Если вы не используете npm, вы можете получить последнюю сборку UMD из unpkg (либо разработки, либо производства сборку). Сборка UMD экспортирует глобальную переменную window.ReactRedux если вы добавите её на страницу с помощью тега <script>.
Компоненты представления и контейнера
Связующие элементы React для Redux поддерживают идею разделения компонентов представления и контейнера. Если вы не знакомы с этими терминами, прочитайте об них сначала, а затем вернитесь. Они важны, поэтому мы подождём!
Вы закончили чтение статьи? Давайте вспомним их различия:
| Компоненты представления | Компоненты контейнера | |
|---|---|---|
| Цель | Как вещи выглядят (разметка, стили) | Как вещи работают (загрузка данных, обновления состояния) |
| Знание Redux | Нет | Да |
| Чтение данных | Чтение данных из свойств | Подписка на состояние Redux |
| Изменение данных | Вызов обратных вызовов из свойств | Отправка действий Redux |
| Пишутся | Вручную | Обычно генерируются React Redux |
Большинство компонентов, которые мы напишем, будут компонентами представления, но нам нужно будет сгенерировать несколько компонентов контейнера, чтобы связать их с хранилищем Redux. Это и техническое задание ниже не подразумевают, что компоненты контейнера должны находиться в верхней части дерева компонентов. Если компонент контейнера становится слишком сложным (т. е. он имеет сильно вложенные компоненты представления с бесчисленными обратными вызовами, передаваемыми вниз), добавьте другой контейнер в древовидную структуру компонентов, как указано в FAQ.
Технически вы могли бы написать компоненты контейнера вручную, используя store.subscribe(). Мы не рекомендуем вам это делать, потому что React Redux производит много оптимизаций производительности, которые трудно реализовать вручную. По этой причине вместо написания компонентов контейнера мы будем генерировать их с помощью функции connect(), предоставляемой React Redux, как вы увидите ниже.
Проектирование иерархии компонентов
Помните, как мы проектировали форму объекта состояния корня? Пришло время спроектировать иерархию пользовательского интерфейса, чтобы она соответствовала ей. Это не задача, специфичная для Redux. Размышление о React — это отличный учебник, который объясняет этот процесс.
Наша задача проста. Мы хотим отобразить список элементов задач. При нажатии на элемент задачи он подчеркивается как завершённый. Мы хотим отобразить поле, где пользователь может добавить новую задачу. В нижней части страницы мы хотим отобразить переключатель для отображения всех задач, только завершённых или только активных задач.
Проектирование компонентов представления
В соответствии с этим заданием, я вижу следующие компоненты представления и их свойства:
-
TodoList— это список отображаемых задач.-
todos: Array— это массив элементов задач со структурой{ id, text, completed }. -
onTodoClick(id: number)— это обратный вызов, который вызывается при нажатии на задачу.
-
-
Todo— это отдельный элемент задачи.-
text: string— это текст, который нужно отобразить. -
completed: boolean— это признак, указывающий, нужно ли отображать задачу зачеркнутой. -
onClick()— это обратный вызов, который вызывается при нажатии на задачу.
-
-
Link— это ссылка с обратным вызовом.-
onClick()— это обратный вызов, который вызывается при нажатии на ссылку.
-
-
Footer— это место, где мы позволяем пользователю изменить текущие отображаемые задачи. -
App— это компонент корня, который отображает все остальные компоненты.
Они описывают внешний вид, но не знают, откуда берутся данные, или как их изменить. Они просто отображают то, что им передано. Если вы перейдёте с Redux на что-то другое, вы сможете сохранить все эти компоненты в точности такими же. Они не зависят от Redux.
Проектирование компонентов контейнера
Нам также понадобятся некоторые компоненты контейнера для подключения компонентов представления к Redux. Например, компоненту представления TodoList нужен контейнер, такой как VisibleTodoList, который подписывается на хранилище Redux и знает, как применить текущий фильтр видимости. Для изменения фильтра видимости мы предоставим компонент-контейнер FilterLink, который отображает Link, который при нажатии отправляет соответствующее действие:
-
VisibleTodoListфильтрует задачи в соответствии с текущим фильтром видимости и отображаетTodoList. -
FilterLinkполучает текущий фильтр видимости и отображаетLink.-
filter: string— это фильтр видимости, который он представляет.
-
Проектирование других компонентов
Иногда сложно сказать, должен ли какой-то компонент быть компонентом представления или контейнера. Например, иногда форма и функция действительно тесно связаны, как в случае с этим компонентом:
-
AddTodo— это поле ввода с кнопкой «Добавить»
Технически мы могли бы разделить его на два компонента, но на этом этапе это может быть слишком рано. В порядке вещей смешивать представление и логику в компоненте, который очень мал. По мере его роста станет более очевидно, как его разделить, поэтому мы оставим его смешанным.
Реализация компонентов
Давайте напишем компоненты! Мы начнём с компонентов представления, чтобы нам не нужно было думать о подключении к Redux.
Реализация компонентов представления
Это все обычные компоненты React, поэтому мы не будем их подробно рассматривать. Мы пишем функциональные бессостоятельные компоненты, если нам не нужно использовать локальное состояние или методы жизненного цикла. Это не означает, что компоненты представления обязательно должны быть функциями — просто так проще их определить. Если и когда вам нужно добавить локальное состояние, методы жизненного цикла или оптимизации производительности, вы можете преобразовать их в классы.
components/Todo.js
import React from 'react'
import PropTypes from 'prop-types'
const Todo = ({ onClick, completed, text }) => (
<li
onClick={onClick}
style={{
textDecoration: completed ? 'line-through' : 'none'
}}
>
{text}
</li>
)
Todo.propTypes = {
onClick: PropTypes.func.isRequired,
completed: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired
}
export default Todo
components/TodoList.js
import React from 'react'
import PropTypes from 'prop-types'
import Todo from './Todo'
const TodoList = ({ todos, onTodoClick }) => (
<ul>
{todos.map(todo => (
<Todo key={todo.id} {...todo} onClick={() => onTodoClick(todo.id)} />
))}
</ul>
)
TodoList.propTypes = {
todos: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
completed: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired
}).isRequired
).isRequired,
onTodoClick: PropTypes.func.isRequired
}
export default TodoList
components/Link.js
import React from 'react'
import PropTypes from 'prop-types'
const Link = ({ active, children, onClick }) => {
if (active) {
return <span>{children}</span>
}
return (
<a
href="#"
onClick={e => {
e.preventDefault()
onClick()
}}
>
{children}
</a>
)
}
Link.propTypes = {
active: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
onClick: PropTypes.func.isRequired
}
export default Link
components/Footer.js
import React from 'react'
import FilterLink from '../containers/FilterLink'
const Footer = () => (
<p>
Show:
{' '}
<FilterLink filter="SHOW_ALL">
All
</FilterLink>
{', '}
<FilterLink filter="SHOW_ACTIVE">
Active
</FilterLink>
{', '}
<FilterLink filter="SHOW_COMPLETED">
Completed
</FilterLink>
</p>
)
export default Footer
components/App.js
import React from 'react'
import Footer from './Footer'
import AddTodo from '../containers/AddTodo'
import VisibleTodoList from '../containers/VisibleTodoList'
const App = () => (
<div>
<AddTodo />
<VisibleTodoList />
<Footer />
</div>
)
export default App
Реализация компонентов контейнера
Теперь пришло время подключить эти компоненты представления к Redux, создав некоторые контейнеры. Технически компонент контейнера — это просто компонент React, который использует store.subscribe() для чтения части дерева состояния Redux и передачи свойств компоненту представления, который он отображает. Вы могли бы написать компонент контейнера вручную, но мы рекомендуем вместо этого генерировать компоненты контейнера с помощью функции connect() библиотеки React Redux, которая предоставляет много полезных оптимизаций, чтобы избежать ненужных перерисовок. (Одним из результатов этого является то, что вам не нужно беспокоиться о рекомендации React по производительности по реализации shouldComponentUpdate самостоятельно.)
Для использования connect(), вам нужно определить специальную функцию, называемую mapStateToProps, которая указывает, как преобразовать текущее состояние хранилища Redux в свойства, которые вы хотите передать компоненту представления, который вы оборачиваете. Например, VisibleTodoList необходимо вычислить todos для передачи TodoList, поэтому мы определяем функцию, которая фильтрует state.todos в соответствии с state.visibilityFilter, и используем её в её mapStateToProps.
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
const mapStateToProps = state => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
В дополнение к чтению состояния, компоненты контейнера могут отправлять действия. Аналогичным образом вы можете определить функцию с названием mapDispatchToProps(), которая получает метод dispatch() и возвращает свойства обратного вызова, которые вы хотите вставить в компонент представления. Например, мы хотим, чтобы VisibleTodoList вставил свойство, называемое onTodoClick в компонент TodoList, и мы хотим, чтобы onTodoClick отправил действие TOGGLE_TODO.
const mapDispatchToProps = dispatch => {
return {
onTodoClick: id => {
dispatch(toggleTodo(id))
}
}
}
Наконец, мы создаём VisibleTodoList вызовом connect() и передачей этих двух функций:
import { connect } from 'react-redux'
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
Это основы API React Redux, но есть несколько сокращений и дополнительных возможностей, поэтому мы рекомендуем вам подробно изучить его документацию. В случае, если вы беспокоитесь о mapStateToProps создании новых объектов слишком часто, вы можете изучить вычисление производных данных с помощью reselect.
Остальные компоненты контейнера определены ниже:
containers/FilterLink.js
import { connect } from 'react-redux'
import { setVisibilityFilter } from '../actions'
import Link from '../components/Link'
const mapStateToProps = (state, ownProps) => {
return {
active: ownProps.filter === state.visibilityFilter
}
}
const mapDispatchToProps = (dispatch, ownProps) => {
return {
onClick: () => {
dispatch(setVisibilityFilter(ownProps.filter))
}
}
}
const FilterLink = connect(
mapStateToProps,
mapDispatchToProps
)(Link)
export default FilterLink
containers/VisibleTodoList.js
import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
const mapStateToProps = state => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
const mapDispatchToProps = dispatch => {
return {
onTodoClick: id => {
dispatch(toggleTodo(id))
}
}
}
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
Реализация других компонентов
containers/AddTodo.js
import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../actions'
let AddTodo = ({ dispatch }) => {
let input
return (
<div>
<form
onSubmit={e => {
e.preventDefault()
if (!input.value.trim()) {
return
}
dispatch(addTodo(input.value))
input.value = ''
}}
>
<input
ref={node => {
input = node
}}
/>
<button type="submit">
Add Todo
</button>
</form>
</div>
)
}
AddTodo = connect()(AddTodo)
export default AddTodo
Передача хранилища
Все компоненты контейнера должны иметь доступ к хранилищу Redux, чтобы они могли подписываться на него. Один из вариантов — передать его в качестве свойства каждому компоненту контейнера. Однако это становится утомительным, так как вам нужно подключать store даже через компоненты представления, просто потому, что они отображают контейнер глубоко в дереве компонентов.
Рекомендуемый вариант — использовать специальный компонент React Redux, называемый <Provider>, чтобы магически сделать хранилище доступным для всех контейнерных компонентов приложения без явного его передачи. Вам нужно использовать его только один раз при отрисовке корневого компонента:
index.js
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'
let store = createStore(todoApp)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
Следующие шаги
Прочитайте полный исходный код этого руководства, чтобы лучше усвоить полученные знания. Затем переходите к расширенному руководству, чтобы узнать, как обрабатывать сетевые запросы и маршрутизацию!
© 2015–2017 Dan Abramov
Licensed under the MIT License.
http://redux.js.org/docs/basics/UsageWithReact.html