Redux 包教包会(三):使用容器组件和展现组件近一步分离组件状态

在这一部分中,咱们会提出 “容器组件” 和 “展现组件” 的概念,“容器组件” 用于接管 “状态”,“展现组件” 用于渲染界面,其中 “展现组件” 也是 React 诞生的初心,专一于高效的编写用户界面。前端

若是您以为咱们写得还不错,记得 点赞 + 关注 + 评论 三连,鼓励咱们写出更好的教程💪

重构代码:将 TodoList 的状态和渲染分离

欢迎阅读 Redux 包教包会系列:node

此教程属于 React 前端工程师学习路线的一部分,点击可查看所有内容。

展现组件和容器组件

Redux 的出现,经过将 State 从 React 组件剥离,并将其保存在 Store 里面,来确保状态来源的可预测性,你可能以为这样就已经很好了,可是 Redux 的动做还没完,它又进一步提出了展现组件(Presentational Components)和容器组件(Container Components)的概念,将纯展现性的 React 组件和状态进一步抽离。react

当咱们把 Redux 状态循环图中的 View 层进一步拆分时,它看起来是这样的:git

即咱们在最终渲染界面的组件和 Store 中存储的 State 之间又加了一层,咱们称这一层为它专门负责接收来自 Store 的 State,并把组件中想要发起的状态改变组装成 Action,而后经过 dispatch 函数发出。github

将状态完全剥离以后剩下的那层称之为展现组件,它专门接收来自容器组件的数据,而后将其渲染成 UI 界面,并在须要改变状态时,告知容器组件,让其代为 dispatch Action。redux

首先,咱们将 App.js 中的 VisibilityFilters 移动到了 src/actions/index.js 中。由于 VisibilityFilters 定义了过滤展现 TodoList 的三种操做,和 Action 的含义更相近一点,因此咱们将类似的东西放在了一块儿。修改 src/actions/index.js 以下:segmentfault

let nextTodoId = 0;

export const addTodo = text => ({
  type: "ADD_TODO",
  id: nextTodoId++,
  text
});

export const toggleTodo = id => ({
  type: "TOGGLE_TODO",
  id
});

export const setVisibilityFilter = filter => ({
  type: "SET_VISIBILITY_FILTER",
  filter
});

export const VisibilityFilters = {
  SHOW_ALL: "SHOW_ALL",
  SHOW_COMPLETED: "SHOW_COMPLETED",
  SHOW_ACTIVE: "SHOW_ACTIVE"
};

编写容器组件

容器组件其实也是一个 React 组件,它只是将原来从 Store 到 View 的状态和从组件中 dispatch Action 这两个逻辑从原组件中抽离出来。设计模式

根据 Redux 的最佳实践,容器组件通常保存在 containers 文件夹中,咱们在 src 文件夹下创建一个 containers 文件夹,而后在里面新建 VisibleTodoList.js 文件,用来表示原 TodoList.js 的容器组件,并在文件中加入以下代码:前端工程师

import { connect } from "react-redux";
import { toggleTodo } from "../actions";
import TodoList from "../components/TodoList";
import { VisibilityFilters } from "../actions";

const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case VisibilityFilters.SHOW_ALL:
      return todos;
    case VisibilityFilters.SHOW_COMPLETED:
      return todos.filter(t => t.completed);
    case VisibilityFilters.SHOW_ACTIVE:
      return todos.filter(t => !t.completed);
    default:
      throw new Error("Unknown filter: " + filter);
  }
};

const mapStateToProps = state => ({
  todos: getVisibleTodos(state.todos, state.filter)
});

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

export default connect(mapStateToProps, mapDispatchToProps)(TodoList);

能够看到,上面的代码主要作了这几件事情:函数

  • 咱们定义了一个 mapStateToProps ,这是咱们以前详细讲解过,它主要是能够获取到来自 Redux Store 的 State 以及组件自身的原 Props,而后组合这二者成新的 Props,而后传给组件,这个函数是 Store 到组件的惟一接口。这里咱们将以前定义在 App.js 中的 getVisibleTodos 函数移过来,并根据 state.filter 过滤条件返回相应须要展现的 todos
  • 接着咱们定义了一个没见过的 mapDispatchToProps 函数,这个函数接收两个参数:dispatchownProps,前者咱们很熟悉了就是用来发出更新动做的函数,后者就是原组件的 Props,它是一个可选参数,这里咱们没有声明它。咱们主要在这个函数声明式的定义全部须要 dispatch 的 Action 函数,并将其做为 Props 传给组件。这里咱们定义了一个 toggleTodo 函数,使得在组件中经过调用 toggleTodo(id) 就能够 dispatch(toggleTodo(id))
  • 最后咱们经过熟悉的 connect 函数接收 mapStateToPropsmapDispatchToProps并调用,而后再接收 TodoList 组件并调用,返回最终的导出的容器组件。

编写展现组件

当咱们编写了 TodoList 的容器组件以后,接着咱们要考虑就是抽离了 State 和 dispatch 的关于 TodoList 的展现组件了。

打开 src/components/TodoList.js 对文件作出相应的改动以下:

import React from "react";
import PropTypes from "prop-types";
import Todo from "./Todo";

const TodoList = ({ todos, toggleTodo }) => (
  <ul>
    {todos.map(todo => (
      <Todo key={todo.id} {...todo} onClick={() => toggleTodo(todo.id)} />
    ))}
  </ul>
);

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

export default TodoList;

在上面的代码中,咱们删除了 connecttoggleTodo Action,并将 TodoList 接收的 dispatch 属性删除,转而改为经过 mapDispatchToProps 传进来的 toggleTodo 函数,并在 Todo 被点击时调用 toggleTodo 函数。

固然咱们的 toggleTodo 属性又回来了,因此咱们在 propTypes 中恢复以前删除的 toggleTodo 。:)

最后,咱们再也不须要 connect()(TodoList),由于 VisibleTodoList.js 中定义的 TodoList 的对应容器组件会取到 Redux Store 中的 State,而后传给 TodoList。

能够看到,TodoList 不用再考虑状态相关的操做,只须要专心地作好界面的展现和动做的响应。咱们进一步将状态与渲染分离,让合适的人作 TA 最擅长的事。

一些琐碎的收尾工做

由于咱们将原来的 TodoList 剥离成了容器组件和 展现组件,因此咱们要将 App.js 里面对应的 TodoList 换成咱们的 VisibleTodoList,由容器组件来提供原 TodoList 对外的接口。

咱们打开 src/components/App.js 对相应的内容做出以下修改:

import React from "react";
import AddTodo from "./AddTodo";
import VisibleTodoList from "../containers/VisibleTodoList";
import Footer from "./Footer";

import { connect } from "react-redux";

class App extends React.Component {
  render() {
    const { filter } = this.props;

    return (
      <div>
        <AddTodo />
        <VisibleTodoList />
        <Footer filter={filter} />
      </div>
    );
  }
}

const mapStateToProps = (state, props) => ({
  filter: state.filter
});

export default connect(mapStateToProps)(App);

能够看到咱们作了这么几件事:

  • 将以前的 TodoList 更换成 VisibleTodoList。
  • 删除 VisibilityFilters,由于它已经被放到了 src/actions/index.js
  • 删除 getVisibleTodos,由于它已经被放到了 VisibleTodoList 中。
  • 删除 mapStateToProps 中获取 todos 的操做,由于咱们已经在 VisibleTodoList 中获取了。
  • 删除对应在 App 组件中的 todos

接着咱们处理一下因 VisibilityFilters 变更而引发的其余几个文件的导包问题。

打开 src/components/Footer.js 修改导包路径:

import React from "react";
import Link from "./Link";
import { VisibilityFilters } from "../actions";

import { connect } from "react-redux";
import { setVisibilityFilter } from "../actions";

const Footer = ({ filter, dispatch }) => (
  <div>
    <span>Show: </span>
    <Link
      active={VisibilityFilters.SHOW_ALL === filter}
      onClick={() => dispatch(setVisibilityFilter(VisibilityFilters.SHOW_ALL))}
    >
      All
    </Link>
    <Link
      active={VisibilityFilters.SHOW_ACTIVE === filter}
      onClick={() =>
        dispatch(setVisibilityFilter(VisibilityFilters.SHOW_ACTIVE))
      }
    >
      Active
    </Link>
    <Link
      active={VisibilityFilters.SHOW_COMPLETED === filter}
      onClick={() =>
        dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))
      }
    >
      Completed
    </Link>
  </div>
);

export default connect()(Footer);

打开 src/reducers/filter.js 修改导包路径:

import { VisibilityFilters } from "../actions";

const filter = (state = VisibilityFilters.SHOW_ALL, action) => {
  switch (action.type) {
    case "SET_VISIBILITY_FILTER":
      return action.filter;
    default:
      return state;
  }
};

export default filter;

由于咱们在 src/actions/index.js 中的 nextTodoId 是从 0 开始自增的,因此以前咱们定义的 initialTodoState 会出现一些问题,好比新添加的 todo 的 id 会与初始的重叠,致使出现问题,因此咱们删除 src/reducers/todos.js 中对应的 initialTodoState,而后给 todos reducer 的 state 赋予一个 [] 的默认值。

const todos = (state = [], action) => {
  switch (action.type) {
    case "ADD_TODO": {
      return [
        ...state,
        {
          id: action.id,
          text: action.text,
          completed: false
        }
      ];
    }

    case "TOGGLE_TODO": {
      return state.map(todo =>
        todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
      );
    }

    default:
      return state;
  }
};

export default todos;

小结

保存修改的内容,你会发现咱们的待办事项小应用依然能够完整的运行,可是咱们已经成功的将原来的 TodoList 分离成了容器组件的 VisibleTodoList 以及展现组件的 TodoList 了。

重构代码:将 Footer 的状态和渲染分离

咱们趁热打铁,用上一节学到的知识来立刻将 Footer 组件的状态和渲染抽离。

编写容器组件

咱们在 src/containers 文件夹下建立一个 FilterLink.js 文件,添加对应的内容以下:

import { connect } from "react-redux";
import { setVisibilityFilter } from "../actions";
import Link from "../components/Link";

const mapStateToProps = (state, ownProps) => ({
  active: ownProps.filter === state.filter
});

const mapDispatchToProps = (dispatch, ownProps) => ({
  onClick: () => dispatch(setVisibilityFilter(ownProps.filter))
});

export default connect(mapStateToProps, mapDispatchToProps)(Link);

能够看到咱们作了如下几件工做:

  • 定义 mapStateToProps,它负责比较 Redux Store 中保存的 State 的 state.filter 属性和组件接收父级传下来的 ownProps.filter 属性是否相同,若是相同,则把 active 设置为 true
  • 定义 mapDispatchToProps,它经过返回一个 onClick 函数,当组件点击时,调用生成一个 dispatch Action,将此时组件接收父级传下来的 ownProps.filter 参数传进 setVisibilityFilter ,生成 action.type"SET_VISIBILITY_FILTER" 的 Action,并 dispatch 这个 Action。
  • 最后咱们经过 connect 组合这二者,将对应的属性合并进 Link 组件并导出。咱们如今应该能够在 Link 组件中取到咱们在上面两个函数中定义的 activeonClick 属性了。

编写展现组件

接着咱们来编写原 Footer 的展现组件部分,打开 src/components/Footer.js 文件,对相应的内容做出以下的修改:

import React from "react";
import FilterLink from "../containers/FilterLink";
import { VisibilityFilters } from "../actions";

const Footer = () => (
  <div>
    <span>Show: </span>
    <FilterLink filter={VisibilityFilters.SHOW_ALL}>All</FilterLink>
    <FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>Active</FilterLink>
    <FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>Completed</FilterLink>
  </div>
);

export default Footer;

能够看到上面的代码修改作了这么几件工做:

  • 咱们将以前的导出 Link 换成了 FilterLink 。请注意当组件的状态和渲染分离以后,咱们将使用容器组件为导出给其余组件使用的组件。
  • 咱们使用 FilterLink 组件,并传递对应的三个 FilterLink 过滤器类型。
  • 接着咱们删除再也不不须要的 connectsetVisibilityFilter 导出。
  • 最后删除再也不须要的filterdispatch 属性,由于它们已经在 FilterLink 中定义并传给了 Link 组件了。

删除没必要要的内容

当咱们将 Footer 中的状态和渲染拆分以后,src/components/App.js 对应的 Footer 相关的内容就再也不须要了,咱们对文件中对应的内容做出以下修改:

import React from "react";
import AddTodo from "./AddTodo";
import VisibleTodoList from "../containers/VisibleTodoList";
import Footer from "./Footer";

class App extends React.Component {
  render() {
    return (
      <div>
        <AddTodo />
        <VisibleTodoList />
        <Footer />
      </div>
    );
  }
}

export default App;

能够看到咱们作了以下工做:

  • 删除 App 组件中对应的 filter 属性和 mapStateToProps 函数,由于咱们已经在 FilterLink 中获取了对应的属性,因此咱们再也不须要直接从 App 组件传给 Footer 组件了。
  • 删除对应的 connect 函数。
  • 删除对应 connect(mapStateToProps)(),由于 App 再也不须要直接从 Redux Store 中获取内容了。

小结

保存修改的内容,你会发现咱们的待办事项小应用依然能够完整的运行,可是咱们已经成功的将原来的 Footer 分离成了容器组件的 FilterLink 以及展现组件的 Footer 了。

重构代码: 将 AddTodo 的状态和渲染分离

让咱们来完成最后一点收尾工做,将 AddTodo 组件的状态和渲染分离。

编写容器组件

咱们在 src/containers 文件夹中建立 AddTodoContainer.js 文件,在其中添加以下内容:

import { connect } from "react-redux";
import { addTodo } from "../actions";
import AddTodo from "../components/AddTodo";

const mapStateToProps = (state, ownProps) => {
  return ownProps;
};

const mapDispatchToProps = dispatch => ({
  addTodo: text => dispatch(addTodo(text))
});

export default connect(mapStateToProps, mapDispatchToProps)(AddTodo);

能够看到咱们作了几件熟悉的工做:

  • 定义 mapStateToProps,由于 AddTodo 不须要从 Redux Store 中取内容,因此 mapStateToProps 只是单纯地填充 connect 的第一个参数,而后简单地返回组件的原 props,不起其它做用。
  • 定义 mapDispatchToProps,咱们定义了一个 addTodo 函数,它接收 text ,而后 dispatch 一个 action.type"ADD_TODO" 的 Action。
  • 最后咱们经过 connect 组合这二者,将对应的属性合并进 AddTodo 组件并导出。咱们如今应该能够在 AddTodo 组件中取到咱们在上面两个函数中定义的 addTodo 属性了。

编写展现组件

接着咱们来编写 AddTodo 的展现组件部分,打开 src/components/AddTodo.js 文件,对相应的内容做出以下的修改:

import React from "react";

const AddTodo = ({ addTodo }) => {
  let input;

  return (
    <div>
      <form
        onSubmit={e => {
          e.preventDefault();
          if (!input.value.trim()) {
            return;
          }
          addTodo(input.value);
          input.value = "";
        }}
      >
        <input ref={node => (input = node)} />
        <button type="submit">Add Todo</button>
      </form>
    </div>
  );
};

export default AddTodo;

能够看到,上面的代码作了这么几件工做:

  • 咱们删除了导出的 connect 函数,而且去掉了其对 AddTodo 的包裹。
  • 咱们将 AddTodo 接收的属性从 dispatch 替换成从 AddTodoContainer 传过来的 addTodo 函数,当表单提交时,它将被调用,dispatch 一个 action.type"ADD_TODO"textinput.value 的 Action。

修改对应的内容

由于咱们将原 TodoList 分离成了容器组件 AddTodoContainer 和展现组件 TodoList,因此咱们须要对 src/components/App.js 作出以下的修改:

import React from "react";
import AddTodoContainer from "../containers/AddTodoContainer";
import VisibleTodoList from "../containers/VisibleTodoList";
import Footer from "./Footer";

class App extends React.Component {
  render() {
    return (
      <div>
        <AddTodoContainer />
        <VisibleTodoList />
        <Footer />
      </div>
    );
  }
}

export default App;

能够看到咱们使用 AddTodoContainer 替换了原来的 AddTodo 导出,并在 render 方法中渲染 AddTodoContainer 组件。

小结

保存修改的内容,你会发现咱们的待办事项小应用依然能够完整的运行,可是咱们已经成功的将原来的 AddTodo 分离成了容器组件的 AddTodoContainer 以及展现组件的 AddTodo 了。

总结

到目前为止,咱们就已经学习完了 Redux 的全部基础概念,而且运用这些基础概念将一个纯 React 版的待办事项一步一步重构到了 Redux。

让咱们最后一次祭出 Redux 状态循环图,回顾咱们在这篇教程中学到的知识:

咱们在这篇教程中首先提出了 Redux 的三大概念:Store,Action,Reducers:

  • Store 用来保存整个应用的状态,这个状态是一个被称之为 State 的 JavaScript 对象。全部应用的状态都是从 Store 中获取,因此状态的改变都是改变 Store 中的状态,因此 Store 也有着 “数据的惟一真相来源” 的称号。
  • Action 是 Redux 中用来改变 Store 状态的惟一手段,全部状态的改变都是以相似 { type: 'ACTION_TYPE', data1, data2 } 这样的形式声明式的定义一个 Action,而后经过 dispatch 这个 Action 来发生的。
  • Reducers 是用来响应 Action 发出的改变更做,经过 switch 语句匹配 action.type ,经过对 State 的属性进行增删改查,而后返回一个新 State 的操做。同时它也是一个纯函数,即不会直接修改 State 自己。

具体反映到咱们重构的待办事项项目里,咱们使用 Store 保存的状态来替换以前 React 中的 this.state,使用 Action 来代替以前 React 发起修改 this.state 的动做,经过 dispatch Action 来发起修改 Store 中状态的操做,使用 Reducers 代替以前 React 中更新状态的 this.setState 操做,纯化的更新 Store 里面保存的 State。

接着咱们趁热打铁,使用以前学到的三大概念,将整个待办事情的剩下部分重构到了 Redux。

可是重构完咱们发现,咱们如今的 rootReducer 函数已经有点臃肿了,它包含了 todosfilter 两类不一样的状态属性,而且若是咱们想要继续扩展这个待办事项应用,那么还会继续添加不一样的状态属性,到时候各类状态属性的操做夹杂在一块儿很容易形成混乱和下降代码的可读性,不利于维护,所以咱们提出了 combineReducers 方法,用于切分 rootReducer 到多个分散在不一样文件的保存着单一状态属性的 Reducer,,而后经过 combineReducers 来组合这些拆分的 Reducers。

详细讲解 combineReducers 的概念以后,咱们接着将以前的不彻底重构的 Redux 代码进行了又一次重构,将 rootReducer 拆分红了 todosfilter 两个 Reducer。

最后咱们更进一步,让 React 专一作好它擅长的编写用户界面的事情,让应用的状态和渲染分离,咱们提出了展现组件和容器组件的概念,前者是完彻底全的 React,接收来自后者的数据,而后负责将数据高效正确的渲染;前者负责响应用户的操做,而后交给后者发出具体的指令,能够看到,当咱们使用 Redux 以后,咱们在 React 上盖了一层逻辑,这层逻辑彻底负责状态方面的工做,这就是 Redux 的精妙之处啊!

但愿看到这里的同窗能对 Redux 有个很好的了解,并能灵活的结合 React 和 Redux 的使用,感谢你的阅读!

One More Thing!

细心的读者可能发现了,咱们画的 Redux 状态循环图都是单向的,它有一个明确的箭头指向,这其实也是 Redux 的哲学,即 ”单向数据流“,也是 React 社区推崇的设计模式,再加上 Reducer 的纯函数约定,这使得咱们整个应用的每一次状态更改都是能够被记录下来,而且能够重现出来,或者说状态是可预测的,它能够追根溯源的找到某一次状态的改变时由某一个 Action 发起的,因此 Redux 也被冠名为 ”可预测的状态管理容器“。

此教程属于 React 前端工程师学习路线的一部分,点击可查看所有内容。

想要学习更多精彩的实战技术教程?来图雀社区逛逛吧。

相关文章
相关标签/搜索