牛骨文教育服务平台(让学习变的简单)

搭配 React

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

尽管如此,Redux 还是和 ReactDeku 这类框架搭配起来用最好,因为这类框架允许你以 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);

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

下一步

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