Spec-Zone.ru › Redux 3

Использование с 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 даже через компоненты представления, просто потому, что они отображают контейнер глубоко в дереве компонентов.

END_OF_DOCUMENT_MARKER

Рекомендуемый вариант — использовать специальный компонент 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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API