原文

From the very beginning, we need to stress that Redux has no relation to React. You can write Redux apps with React, Angular, Ember, jQuery, or vanilla JavaScript.

在开始之前,需要强调一下,Redux和React没有关系。可以使用React,Angular,Ember,jQuery或原生的JavaScript编写Redux应用程序。

That said, Redux works especially well with libraries like React and Deku because they let you describe UI as a function of state, and Redux emits state updates in response to actions.

另一方面,Redux擅长和ReactDeku这样将UI表示状态函数的库一起使用,Redux在响应状态变化后会派发状态事件。

We will use React to build our simple todo app.

接下来使用React构建简单的Todo应用程序。

安装React-Redux库(Installing React Redux)

React bindings are not included in Redux by default. You need to install them explicitly:

React绑定默认不包含在Redux中。需要单独安装:

npm install --save react-redux

If you don’t use npm, you may grab the latest UMD build from unpkg (either a development or a production build). The UMD build exports a global called window.ReactRedux if you add it to your page via a <script> tag.

如果不使用npm包管理器,可以从unpkg下载最新的UMD版本(开发版压缩版。UMD版本暴露出全局变量window.ReactRedux,可以通过<script>标签将其引入。

展示和容器组件(Presentational and Container Components)

React bindings for Redux embrace the idea of separating presentational and container components. If you’re not familiar with these terms, read about them first, and then come back. They are important, so we’ll wait!

React-Redux绑定鼓励展示和容器组件分离。如果你对两者的不熟悉,建议首先阅读它们的相关资料,然后再继续阅读这篇文章。这两者非常重要,一定要先了解一下。

Finished reading the article? Let’s recount their differences:

  Presentational Components Container Components
Purpose How things look (markup, styles) How things work (data fetching, state updates)
Aware of Redux No Yes
To read data Read data from props Subscribe to Redux state
To change data Invoke callbacks from props Dispatch Redux actions
Are written By hand Usually generated by React Redux

阅读玩这篇文章后,总结这两者的区别如下:

  展示组件 容器组件
目的 如何展示(标签,样式) 如何工作(数据获取,状态更新)
Redux协作
读取数据 读取Props 订阅Redux State
更新数据 回调Props 派发Redux Action
生成方式 手写 通常由React Redux生成

Most of the components we’ll write will be presentational, but we’ll need to generate a few container components to connect them to the Redux store. This and the design brief below do not imply container components must be near the top of the component tree. If a container component becomes too complex (i.e. it has heavily nested presentional components with countless callbacks being passed down), introduce another container within the component tree as noted in the FAQ.

程序中的大多数组件都是用来展示UI的,但是需要生成一些容器组件来将他们和Redux Store连接起来。这并不是说容器组件必须位于组件树的顶层。如果容器组件过于复杂(比如,包含大量的展示组件和数不清的回调在向下传递),建议参考FAQ将容器组件拆分。

Technically you could write the container components by hand using store.subscribe(). We don’t advise you to do this because React Redux makes many performance optimizations that are hard to do by hand. For this reason, rather than write container components, we will generate them using the connect() function provided by React Redux, as you will see below.

技术上可以在编写的容器组件中手工处理store.subscribe(),但我们不建议你这么做,React-Redux中包含了很多性能优化细节。所以使用React-Redux提供的connect()方法生成容器组件,后面会逐步演示。

设计组件层次(Designing Component Hierarchy)

Remember how we designed the shape of the root state object? It’s time we design the UI hierarchy to match it. This is not a Redux-specific task. Thinking in React is a great tutorial that explains the process.

还记得如何设计状态的结构吗?现在需要设计其对应的UI层次结构。当然,这不是使用Redux的特定步骤。React设计思想是一份讲解如何设计UI层次结构的优秀教材。

Our design brief is simple. We want to show a list of todo items. On click, a todo item is crossed out as completed. We want to show a field where the user may add a new todo. In the footer, we want to show a toggle to show all, only completed, or only active todos.

接下来要用的设计简介是非常简单的。展示一个Todo的列表;点击某项Todo时,勾划掉表示完成;放置一个表单用来让用户输入新的Todo项;在底部显示一个过滤器,用来决定显示全部,还是显示已完成,或者显示未完成列表。

展示组件设计(Designing Presentational Components)

I see the following presentational components and their props emerge from this brief:

  • TodoList is a list showing visible todos.
    • todos: Array is an array of todo items with { id, text, completed } shape.
    • onTodoClick(id: number) is a callback to invoke when a todo is clicked.
  • Todo is a single todo item.
    • text: string is the text to show.
    • completed: boolean is whether todo should appear crossed out.
    • onClick() is a callback to invoke when a todo is clicked.
  • Link is a link with a callback.
    • onClick() is a callback to invoke when link is clicked.
  • Footer is where we let the user change currently visible todos.
  • App is the root component that renders everything else.

根据需求可以设计出下列组件和相关的属性概要:

  • TodoList,Todo列表显示组件。
    • todos: Array,Todo项目数组,每一项包含{ id, text, completed }3个属性;
    • onTodoClick(id: number),Todo点击事件的回调函数。
  • Todo,单独的Todo组件。
    • text: string,要显示的Todo文本内容;
    • completed: boolean,决定是否显示位勾划状态;
    • onClick() Todo点击事件的回调函数。
  • Link,一个包含回调的点击组件。
    • onClick(),点击事件的回调函数。
  • Footer,用户切换显示条件的组件。
  • App渲染的根组件。

They describe the look but don’t know where the data comes from, or how to change it. They only render what’s given to them. If you migrate from Redux to something else, you’ll be able to keep all these components exactly the same. They have no dependency on Redux.

这些组件描述了应用程序的外观,但是它们不知道数据从哪儿来,也不知道如何改变这些数据。这些组件仅仅是用来根据输入值进行渲染。如果需要将项目从Redux中迁移出去,这一部分不需要做任何更改。它们是跟Redux无关的。

容器组件设计(Designing Container Components)

We will also need some container components to connect the presentational components to Redux. For example, the presentational TodoList component needs a container like VisibleTodoList that subscribes to the Redux store and knows how to apply the current visibility filter. To change the visibility filter, we will provide a FilterLink container component that renders a Link that dispatches an appropriate action on click:

  • VisibleTodoList filters the todos according to the current visibility filter and renders a TodoList.
  • FilterLink gets the current visibility filter and renders a Link.
    • filter: string is the visibility filter it represents.

应用程序还需要一些容器组件用来连接展示组件和Redux。比如,展示组件TodoList需要容器组件VisibleTodoList来订阅Redux Store,并且将根据当前的显示条件进行过滤筛选。如果要改变显示条件,需要容器组件FilterLink用来渲染Link并处理Click事件:

  • VisibleTodoList,根据当前显示条件对数据进行筛选并渲染TodoList
  • FilterLink,获取当前显示条件并渲染一个Link
    • filter: string,表示当前的显示条件。

其他组件设计(Designing Other Components)

Sometimes it’s hard to tell if some component should be a presentational component or a container. For example, sometimes form and function are really coupled together, such as in case of this tiny component:

  • AddTodo is an input field with an “Add” button

某些情况下,真的很难判断一个组件是展示组件还是容器组件。比如,表单和相关的处理函数有时是在一起的,比如下面这个小组件:

  • AddTodo,一个输入表单配合一个“添加”按钮。

Technically we could split it into two components but it might be too early at this stage. It’s fine to mix presentation and logic in a component that is very small. As it grows, it will be more obvious how to split it, so we’ll leave it mixed.

技术上可以将这个组件拆分为两个组件,但在这个案例拆分还为时过早。这个组件很小,将展示和逻辑混合在一起没有问题。当其变得更大更复杂时更合适拆分,目前让其混合在一起。

组件实现(Implementing Components)

Let’s write the components! We begin with the presentational components so we don’t need to think about binding to Redux yet.

现在开始编写组件。先从展示组件开始,所以目前暂时不需要考虑如何集成Redux。

展示组件实现(Implementing Presentational Components)

These are all normal React components, so we won’t examine them in detail. We write functional stateless components unless we need to use local state or the lifecycle methods. This doesn’t mean that presentational components have to be functions—it’s just easier to define them this way. If and when you need to add local state, lifecycle methods, or performance optimizations, you can convert them to classes.

这些都是普通的React组件,简单将其编写如下。下面的组件不需要本地状态和生命周期方法,所以使用无状态的Function形式编码。这并不是说展示组件必须用Function形式——这里这么写仅仅是因为定义编码起来比较容易。当需要本地状态,生命周期方法,或者性能调优时,将其转换位Class形式即可。

components/Todo.js

import React, { PropTypes } from 'react'

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, { PropTypes } from 'react'
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, { PropTypes } from 'react'

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

实现容器组件(Implementing Container Components)

Now it’s time to hook up those presentational components to Redux by creating some containers. Technically, a container component is just a React component that uses store.subscribe() to read a part of the Redux state tree and supply props to a presentational component it renders. You could write a container component by hand, but we suggest instead generating container components with the React Redux library’s connect() function, which provides many useful optimizations to prevent unnecessary re-renders. (One result of this is that you shouldn’t have to worry about the React performance suggestion of implementing shouldComponentUpdate yourself.)

现在是时候创建容器组件将相关的展示组件和Redux连接起来了。从技术上讲,容器组件也是一个React组件,其使用store.subscribe()方法读取部分Redux状态树,并将相关状态通过Props传递给展示组件以供渲染。可以手写容器组件,但建议使用React-Redux绑定哭提供的connect()方法实现,这个库内置了很多有用的性能优化策略来阻止不必要的重新渲染。(一个直接的结果是必用总是根据React performance suggestion来实现相关的shouldComponentUpdate)。

To use connect(), you need to define a special function called mapStateToProps that tells how to transform the current Redux store state into the props you want to pass to a presentational component you are wrapping. For example, VisibleTodoList needs to calculate todos to pass to the TodoList, so we define a function that filters the state.todos according to the state.visibilityFilter, and use it in its mapStateToProps:

使用connect()需要定义一个特殊的mapStateToProps方法用来指定如何将Redux的状态转化为展示组件需要的Props。比如,VisibleTodoList需要将todos的计算结果传递给TodoList,所以定义一个专门的方法根据state.visibilityFilter来过滤state.todos,并将其在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)
  }
}

In addition to reading the state, container components can dispatch actions. In a similar fashion, you can define a function called mapDispatchToProps() that receives the dispatch() method and returns callback props that you want to inject into the presentational component. For example, we want the VisibleTodoList to inject a prop called onTodoClick into the TodoList component, and we want onTodoClick to dispatch a TOGGLE_TODO action:

容器组件除了读取状态之外,还需要派发Action。同理要定义一个mapDispatchToProps()方法,用以接收dispatch()方法,并返回需要注入到展示组件中的回调函数。例如,需要在VisibleTodoList中注入一个onTodoClick属性到TodoList组件中,同时需要在onTodoClick中派发一个TOGGLE_TODOAction:

const mapDispatchToProps = (dispatch) => {
  return {
    onTodoClick: (id) => {
      dispatch(toggleTodo(id))
    }
  }
}

Finally, we create the VisibleTodoList by calling connect() and passing these two functions:

最终,在connect()方法中传入两个定义好的方法返回一个VisibleTodoList组件:

import { connect } from 'react-redux'

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

export default VisibleTodoList

These are the basics of the React Redux API, but there are a few shortcuts and power options so we encourage you to check out its documentation in detail. In case you are worried about mapStateToProps creating new objects too often, you might want to learn about computing derived data with reselect.

这是React-Redux的基础API,在其文档中有更过快捷方法和重要配置项的说明。如果你担心mapStateToProps频繁的创建一个新对象,可以学习使用reselectcomputing derived data

Find the rest of the container components defined below:

其他容器组件的定义如下:

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

其他组件实现(Implementing Other Components)

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

传递Store对象(Passing the Store)

All container components need access to the Redux store so they can subscribe to it. One option would be to pass it as a prop to every container component. However it gets tedious, as you have to wire store even through presentational components just because they happen to render a container deep in the component tree.

所有的容器组件都需要访问Redux的Store对象,以完成订阅。一个可行的方法是将其作为每一个容器组件的属性。但这样做过于单调,你必须在每一个组件中将其传递以保证整个组件树中该对象都可用。

The option we recommend is to use a special React Redux component called <Provider> to magically make the store available to all container components in the application without passing it explicitly. You only need to use it once when you render the root component:

建议的方法是使用一个特殊的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')
)

下一步(Next Steps)

Read the complete source code for this tutorial to better internalize the knowledge you have gained. Then, head straight to the advanced tutorial to learn how to handle network requests and routing!

通读教程相关的完整源码以更好的理解相关概念。然后继续学习进阶教程学习如何处理异步请求和路由。