Skip to content

在 React 中使用 Redux #2

Open
@collinxz-coder

Description

@collinxz-coder

前言

上一篇文章中,我们讲了 Redux 的基本概念和一个简单的 Demo,今天我们讲讲如何在 React 中使用 Redux。在开始之前,建议大家去了解下容器组件和展示组件相分离的开发思想。虽然这篇文章的原作者已经对该思想有了新的观点,不再那么推崇这种分离方式了。但它确实可以帮助我们把有状态的逻辑与存粹的展示界面分离开。

容器组件与展示组件

这部分内容来自于 Redux 官方文档

大家应该已经看过了前言中提到的容器组件与展示组件分离了吧。没看过也不因影响,我们下面列举出了容器组件与展示组件各自的指责。

展示组件 容器组件
作用 组件界面的展示 数据的获取、状态更新等
是否直接使用 Redux
数据来源 props 监听 Redux state
数据修改 从 props 调用回调函数 向 Redux 派发 actions
调用方式 手动 通常由 React Redux 生成

在我们的应用中,大部分组件都应该是展示型的,但至少一般需要几个组件来将它们和 Redux store 连接起来。虽然从技术上来说,可以使用 store.subscribe() 来编写容器组件,但是不建议这么做的原因是无法使用 Redux React 带来的性能优化。所以最好不要手写容器组件,而是通过 connect() 的方式来生成。

集成 React 的 Todo App

在 React 中使用 Redux 并没有太多的概念以及术语,所以我们就直接从 Demo 开始讲起。我们的 Demo 基于上一篇文章中的 Todo App 的基础来继续开发。如果你没有看上一篇文章,也可以在这里直接获取上一篇文章中的 Demo 源码

依赖

要想在 React 中使用 Redux,我们需要安装 react-redux 包,在项目根目录下使用 npm 或者 yarn 都可以:

yarn add react-redux

程序结构分析

我们这里采用前面提到过的容器组件与展示组件分离的思想来开发我们的 Todo App 项目。在开始实际编码之前,认真的理清程序结构是一个好的习惯。

我们应用的需求是,展示一个 Todos 列表,并支持对 Todos 的增加、删除以及完成。还有根据 Todos 状态来过滤要显示的列表。

展示组件

TodoList

用于展示我们的 Todos 列表,这个组件有两个 props,todos 是一个数组,里面存放了所有的 Todo,以及一个 Todo 被点击时的回调函数 onTodoClick(index)

Todo

一个 Todo 项。它有三个 props, text 是要显示的文本、 completed 用来表示当前 Todo 是否已经被完成,如果被完成了,我们则在这个组件的文本上显示一个删除线,以及一个被点击时的回调函数 onClick()

Link

这个组件展示了我们应用中支持的所有过滤器,通过点击不同的 Link 组件来显示不同状态下的 Todos 列表。这个组件只有一个被点击的回调函数 onClick()

Footer

这个组件中包含了所有的过滤器

App

根组件

展示组件

VisibleTodoList

通过当前 store 中存储的 visibilityFilter 来对 Todo 列表进行过滤,并渲染 Todo 列表。

FilterLink

获得系统所支持的所有过滤器并渲染 Link 列表

混合型组件

当然,一个应用中,不是所有的组件都能够很清晰的将展示组件和容器组件完全分离开来,在我们的应用中,应该还有一个 AddTodo 组件来用于添加新的 Todo,由于这个组件表单和函数严重耦合在了一起,所以我们无法直接将他们定义为容器组件或者是展示组件。当然,也可以将它拆分成一个容器组件和一个展示组件,只是在我们这个应用中,完全没有必要这么做。对于组件的拆分还是要结合实际场景来选择,不能一股脑的套用各种所谓的最佳实践。

我们姑且将这个组件归纳到混合型中把。

让我们开始写代码吧

啰里八嗦的说了一堆,总算是到了开始写代码的时候!!!我们先把 Redux 放到一边,开始先把应用的界面写了。我们先在 src 目录中创建一个名为 components 的目录用户存放我们所有的展示组件

Todo 组件

这个组件的职责是用于展示 Todos 列表中的项,它的所有数据都来自于父组件传递过来的 props 中。

src/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;

这个组件很简单,仅仅只是将 props 中传递过来的文本显示以及通过 completed 来判断是否需要显示删除线,并在被点击的时候,通过 onClick 回调来通知父组件。

这里推荐大家在组件中使用 prop-types 库来帮助我们实现 props 数据类型检测。JS 本身是弱类型语言,我们通过这个库给 props 添加一个约束,在传递了错误的数据类型时,能够很直观的看到错误。

TodoList 组件

这个组件的职责是渲染出传递给他 todos 中 所有的 Todo,在这个组件我们调用了上面的 Todo 组件来渲染列表。

src/components/TodoList.js

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

const TodoList = ({ todos, onTodoClick }) => (
    <ul>
        {todos.map((todo, index) => (
            <Todo key={index} {...todo} onClick={() => onTodoClick(index)} />
        ))}
    </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;

这个组件本身并没有太多要说的,我们这里使用了 PropTypes.arrayOf() 来指定一个数组由某一类型的元素组成。在 arrayOf() 中,使用了 PropTypes.shape() 用于指定数组内的特定元素的结构。

Link 组件

src/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;

这个组件中,我们通过 active 来判断当前 Link 是否处于活动状态,如果组件是活动的,则仅显示文字,否则显示一个 a 标签。点击这个 a 标签会调用父组件传递过来的 onClick 回调来告知我们选择了某个过滤器。

Footer 组件

src/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;

这个组件中,我们使用了 FilterLink 组件,这个组件是一个容器组件,待会我们会说到这个组件。有兴趣的朋友你可以自己 Mock 一些数据传入到我们定义的展示组件中来看看我们前面写界面组件,我们这就不去试了,我们接下来编写我们的容器组件组件。

容器组件的职责是用来进行数据获取以及状态更新等操作,我们现在来创建几个容器组件来将展示组件和 Redux 联系起来。

本质上,容器组件就是使用 store.subscribe() 创建监听器来创建从 Redux 的 state 中获取部分数据并通过 props 传递给展示组件中。但是我们最好不要自己去创建容器组件,而是应该使用 connect() 方法来生成,这个方法做了性能优化来避免了很多的重复渲染。

connect() 方法可以将 state 映射到 props 中,我们通常将这个函数命名为 mapStateToProps,这个函数接受 state 作为参数。我们前面说到了 VisibleTodoList 组件需要过滤 todos 列表。我们定义了 getVisibleTodos 方法来根据 state.visibilityFilter 来过滤 state.todos。

const getVisibleTodos = (todos, filter) => {
    switch (filter) {
        case 'SHOW_COMPLETED':
            return todos.filter(t => t.completed);
        case 'SHOW_ACTIVE':
            return todos.filter(t => !t.completed);
        case 'SHOW_ALL':
        default:
            return todos;
    }
}

现在我们将过滤后的数据传递到 mapStateToProps 中。

const mapStateToProps = state => {
    return {
        todos: getVisibleTodos(state.todos, state.visibilityFilter)
    }
}

我们现在将这个方法传递到 connect() 函数中就可以将 state 传递到 props 中。容器组件除了能够将 state 传递到 props 中,还能够分发 action。通常我们将用于分发的 action 的函数命名为 mapDispatchProps(),这个方法接受 store 的 dispatch() 方法作为参数并返回要注入到展示组件中的 props 中的回调方法。VisibleTodoList 组件中,我们需要向 TodoList 中注入一个 onTodoClick 的 props,并且这个回调方法中需要分发 TOGGLE_TODO 这个 action:

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

这两个方法的名称并不固定是 mapStateToProps 和 mapDispatchToProps,你完全可以定义为你想要的任何名称。

现在我们使用 connect() 方法将 mapStateToPropsmapDispatchToProps 注入到展示组件中。

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

connect() 方法是 React Redux 中提供的 API,这个函数接受4个参数:

mapStateToProps

mapStateToProps 是一个函数,这个函数的原型是:

mapStateToProps?: (state, ownProps?) => Object

任何更新 state 时,mapStateToProps 都会被调用。这个函数必须返回一个普通对象,返回值将被合并到展示组件的 props 中,如果不想提供此参数,则可以使用 null 或者 undefined 来代替。

mapStateToProps 有两个参数,如果只提供 state 参数,则只要 state 改版,就会调用 mapStateToProps,并将 state 作为唯一的参数。如果提供了 ownProps 参数,则只要 state 改变或者展示组件接收到新的 props,就会调用它。 state 将被当作第一个参数,而展示组件的 props 则作为第二个参数。

mapDispatchToProps

这个参数可以是对象或者函数或者不提供这个参数。如果调用 connect() 时不提供这个参数,则会默认将 dispatch 传递进来。如果 mapDispatchToProps 为函数时,函数原型为:

mapDispatchToProps(dispatch, ownProps)

dispatch 可以用于分发 action,ownProps 是可选参数,如果提供了这个参数只要展示组件接收到了新的 props 时,就会重新调用它。

mergeProps

options

更多关于 connect 的文档参见 react-redux 文档

如果你担心 mapStateToProps 创建新对象太过频繁,可以学习如何使用 reselect 来计算衍生数据。

介绍玩 connect() 方法后,我们来开始编码容器组件:

src/containers/VisibleTodoList.js

import { connect } from 'react-redux';
import { completeTodo } from '../action';
import TodoList from '../components/TodoList';

const getVisibleTodos = (todos, filter) => {
    switch(filter) {
        case 'SHOW_COMPLETED':
            return todos.filter(t => t.completed);
        case 'SHOW_ACTIVE':
            return todos.filter(t => !t.completed);
        case 'SHOW_ALL':
        default:
            return todos;
    }
}

const mapStateToProps = state => {
    return {
        todos: getVisibleTodos(state.todos, state.visibilityFilter)
    }
}

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

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

export default VisibleTodoList;

接下来我们定义 FilterLink 组件:

src/containers/FilterLink.js

import { connect } from 'react-redux';
import { setVisibilityFilter } from '../action';
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;

定义完了容器组件,我们接下来定义混合的 AddTodo 组件:

src/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

所有容器组件都写完了,接下来我们定义根 App 组件。

src/components/App.js

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

export default () => (
    <div>
        <AddTodo />
        <VisibleTodoList />
        <Footer />
    </div>
)

好了,我们已经完成了所有的组件,接下来我们到 index.js 文件中,来调用 所有组件。所有的容器组件都可以访问 Redux store,你可以选择手动监听或者是把它已 props 的形式传入到所有容器组件中。但是这种方式并不推荐使用。更好的方式是使用 React Redux 组件提供的 <Provider> 来让所有容器组件都可以访问 store。

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')
)

好了,我们的应用到这里已经完成了,你可以执行 npm start 来看看效果了。怎么样,很简答吧。

最后这里我们附上 React 和 Redux 的交互流程图。

总结

最近工作太忙了,一直没时间补上这篇文章,这篇文章大部分是将 Redux 官网重新讲述了一遍。如果写的不好,您可以在下面指出来,如果觉得还能看的话,希望您能点个 star。

下一篇文章我会带着大家模仿水滴清单做一个完整的应用。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions