搭配 React

这里需要再强调一下:Redux 和 React 之间没有关系。Redux 支持 React、Angular、Ember、jQuery 甚至纯 JavaScript。

尽管如此,Redux 还是和 React 和 Deku 这类框架搭配起来用最好,因为这类框架允许你以 state 函数的形式来描述界面,Redux 通过 action 的形式来发起 state 变化。

下面使用 React 来开发一个 todo 任务管理应用。

安装 React Redux

Redux 默认并不包含 React 绑定库,需要单独安装。

npm install --save react-redux

智能组件(Smart Components)和笨拙组件(Dumb Components)

Redux 的 React 绑定库拥抱了 “智能”组件和“笨拙”组件相分离 的开发思想。

明智的做法是只在最顶层组件(如路由操作)里使用 Redux。内部组件应该像木偶一样保持“呆滞”,所有数据都通过 props 传入。

位置 使用 Redux 读取数据 修改数据
“智能”组件 最顶层,路由处理 是 从 Redux 获取 state 向 Redux 发起 actions
“笨拙”组件 中间和子组件 否 从 props 获取数据 从 props 调用回调函数

在这个 todo 应用中,只应有一个“智能”组件,它存在于组件的最顶层。在复杂的应用中,也有可能会有多个智能组件。虽然你也可以嵌套使用“智能”组件,但应该尽可能的使用传递 props 的形式。

设计组件层次结构

还记得当初如何 设计 reducer 结构 吗?现在就要定义与它匹配的界面的层次结构。其实这不是 Redux 相关的工作,React 开发思想在这方面解释的非常棒。

我们的概要设计很简单。我们想要显示一个 todo 项的列表。一个 todo 项被点击后,会增加一条删除线并标记 completed。我们会显示用户新增一个 todo 字段。在 footer 里显示一个可切换的显示全部/只显示 completed 的/只显示 incompleted 的 todos。

以下的这些组件(和它们的 props )就是从这个设计里来的:

  • AddTodo 输入字段的输入框和按钮。

  • onAddClick(text: string) 当按钮被点击时调用的回调函数。

  • TodoList 用于显示 todos 列表。

  • todos: Array 以 { text, completed } 形式显示的 todo 项数组。

  • onTodoClick(index: number) 当 todo 项被点击时调用的回调函数。

  • Todo 一个 todo 项。

  • text: string 显示的文本内容。

  • completed: boolean todo 项是否显示删除线。
  • onClick() 当 todo 项被点击时调用的回调函数。

  • Footer 一个允许用户改变可见 todo 过滤器的组件。

  • filter: string 当前的过滤器为: "SHOW_ALL"、 "SHOW_COMPLETED" 或 "SHOW_ACTIVE"。

  • onFilterChange(nextFilter: string): 当用户选择不同的过滤器时调用的回调函数。

这些全部都是“笨拙”的组件。它们不知道数据是从哪里来的,或者数据是怎么变化的。你传入什么,它们就渲染什么。

如果你要把 Redux 迁移到别的上,你应该要保持这些组件的一致性。因为它们不依赖 Redux。

直接写就是了!我们已经不用绑定到 Redux。你可以在开发过程中给出一些实验数据,直到它们渲染对了。

笨拙组件

这就是普通的 React 组件,所以就不在详述。直接看代码:

components/AddTodo.js

import React, { findDOMNode, Component, PropTypes } from "react";

export default class AddTodo extends Component {
  render() {
    return (
      <div>
        <input type="text" ref="input" />
        <button onClick={e => this.handleClick(e)}>
          Add
        </button>
      </div>
    );
  }

  handleClick(e) {
    const node = findDOMNode(this.refs.input);
    const text = node.value.trim();
    this.props.onAddClick(text);
    node.value = "";
  }
}

AddTodo.propTypes = {
  onAddClick: PropTypes.func.isRequired
};

components/Todo.js

import React, { Component, PropTypes } from "react";

export default class Todo extends Component {
  render() {
    return (
      <li
        onClick={this.props.onClick}
        style={{
          textDecoration: this.props.completed ? "line-through" : "none",
          cursor: this.props.completed ? "default" : "pointer"
        }}>
        {this.props.text}
      </li>
    );
  }
}

Todo.propTypes = {
  onClick: PropTypes.func.isRequired,
  text: PropTypes.string.isRequired,
  completed: PropTypes.bool.isRequired
};

components/TodoList.js

import React, { Component, PropTypes } from "react";
import Todo from "./Todo";

export default class TodoList extends Component {
  render() {
    return (
      <ul>
        {this.props.todos.map((todo, index) =>
          <Todo {...todo}
                key={index}
                onClick={() => this.props.onTodoClick(index)} />
        )}
      </ul>
    );
  }
}

TodoList.propTypes = {
  onTodoClick: PropTypes.func.isRequired,
  todos: PropTypes.arrayOf(PropTypes.shape({
    text: PropTypes.string.isRequired,
    completed: PropTypes.bool.isRequired
  }).isRequired).isRequired
};

components/Footer.js

import React, { Component, PropTypes } from "react";

export default class Footer extends Component {
  renderFilter(filter, name) {
    if (filter === this.props.filter) {
      return name;
    }

    return (
      <a href="#" onClick={e => {
        e.preventDefault();
        this.props.onFilterChange(filter);
      }}>
        {name}
      </a>
    );
  }

  render() {
    return (
      <p>
        Show:
        {" "}
        {this.renderFilter("SHOW_ALL", "All")}
        {", "}
        {this.renderFilter("SHOW_COMPLETED", "Completed")}
        {", "}
        {this.renderFilter("SHOW_ACTIVE", "Active")}
        .
      </p>
    );
  }
}

Footer.propTypes = {
  onFilterChange: PropTypes.func.isRequired,
  filter: PropTypes.oneOf([
    "SHOW_ALL",
    "SHOW_COMPLETED",
    "SHOW_ACTIVE"
  ]).isRequired
};

就这些,现在开发一个笨拙型的组件 App 把它们渲染出来,验证下是否工作。

containers/App.js

import React, { Component } from "react";
import AddTodo from "../components/AddTodo";
import TodoList from "../components/TodoList";
import Footer from "../components/Footer";

export default class App extends Component {
  render() {
    return (
      <div>
        <AddTodo
          onAddClick={text =>
            console.log("add todo", text)
          } />
        <TodoList
          todos={[{
            text: "Use Redux",
            completed: true
          }, {
            text: "Learn to connect it to React",
            completed: false
          }]}
          onTodoClick={todo =>
            console.log("todo clicked", todo)
          } />
        <Footer
          filter="SHOW_ALL"
          onFilterChange={filter =>
            console.log("filter change", filter)
          } />
      </div>
    );
  }
}

渲染 <App /> 结果如下:

单独来看,并没有什么特别,现在把它和 Redux 连起来。

连接到 Redux

我们需要做出两个变化,将 App 组件连接到 Redux 并且让它能够 dispatch actions 以及从 Redux store 读取到 state。

首先,我们需要获取从之前安装好的 react-redux 提供的 Provider,并且在渲染之前将根组件包装进 <Provider>。

index.js

import React from "react";
import { createStore } from "redux";
import { Provider } from "react-redux";
import App from "./containers/App";
import todoApp from "./reducers";

let store = createStore(todoApp);

let rootElement = document.getElementById("root");
React.render(
  // 为了解决在 React 0.13 的一个问题
  // 子标签必须包装成一个 function。
  <Provider store={store}>
    {() => <App />}
  </Provider>,
  rootElement
);

这使得我们的 store 能为下面的组件所用。(在内部,这个是通过 React 的 "context" 特性实现。)

接着,我们想要通过 react-redux 提供的 connect() 方法将包装好的组件连接到Redux。尽量只做一个顶层的组件,或者 route 处理。从技术上来说你可以将应用中的任何一个组件 connect() 到 Redux store 中,但尽量要避免这么做,因为这个数据流很难追踪。

任何一个从 connect() 包装好的组件都可以得到一个 dispatch 方法作为组件的 props。connect() 的唯一参数是 selector。此方法可以从 Redux store 接收到全局的 state,然后返回一个你的组件中需要的 props。最简单的情况下,可以返回一个初始的 state ,但你可能希望它发生了变化。

为了组合 selectors 更有效率,不妨看看 reselect。在这个例子中我们不会用到它,但它适合更大的应用。

containers/App.js

import React, { Component, PropTypes } from "react";
import { connect } from "react-redux";
import { addTodo, completeTodo, setVisibilityFilter, VisibilityFilters } from "../actions";
import AddTodo from "../components/AddTodo";
import TodoList from "../components/TodoList";
import Footer from "../components/Footer";

class App extends Component {
  render() {
    // Injected by connect() call:
    const { dispatch, visibleTodos, visibilityFilter } = this.props;
    return (
      <div>
        <AddTodo
          onAddClick={text =>
            dispatch(addTodo(text))
          } />
        <TodoList
          todos={this.props.visibleTodos}
          onTodoClick={index =>
            dispatch(completeTodo(index))
          } />
        <Footer
          filter={visibilityFilter}
          onFilterChange={nextFilter =>
            dispatch(setVisibilityFilter(nextFilter))
          } />
      </div>
    );
  }
}

App.propTypes = {
  visibleTodos: PropTypes.arrayOf(PropTypes.shape({
    text: PropTypes.string.isRequired,
    completed: PropTypes.bool.isRequired
  })),
  visibilityFilter: PropTypes.oneOf([
    "SHOW_ALL",
    "SHOW_COMPLETED",
    "SHOW_ACTIVE"
  ]).isRequired
};

function selectTodos(todos, filter) {
  switch (filter) {
  case VisibilityFilters.SHOW_ALL:
    return todos;
  case VisibilityFilters.SHOW_COMPLETED:
    return todos.filter(todo => todo.completed);
  case VisibilityFilters.SHOW_ACTIVE:
    return todos.filter(todo => !todo.completed);
  }
}

// Which props do we want to inject, given the global state?
// Note: use https://github.com/faassen/reselect for better performance.
function select(state) {
  return {
    visibleTodos: selectTodos(state.todos, state.visibilityFilter),
    visibilityFilter: state.visibilityFilter
  };
}

// Wrap the component to inject dispatch and state into it
export default connect(select)(App);

到此为止,迷你型的任务管理应用就开发完毕。

下一步

参照 本示例完整 来深化理解。然后就可以跳到 高级教程 学习网络请求处理和路由。

文章导航