banner
他山之石

他山之石

react-redux开发简单的To do应用

redux 专注于状态管理,和 react 解耦,为了方便使用,redux 的作者封装了一个 react 专用的库 react-redux。

image

前面通过一个简单的计数器应用来学习 redux 的基本原理和用法,在本篇文章,将介绍 redux 如何与 react 结合使用。
为了方便使用,redux 的作者封装了一个 react 专用的库 react-redux,这也是接下来的重点。

本文完整代码请查看 Github:https://github.com/YanYuanFE/redux-app

// clone repo
git clone https://github.com/YanYuanFE/redux-app.git


cd redux-app

// checkout branch
git checkout part-2

// install
npm install

// start
npm start

安装#

npm install react-redux --save

基本 API#

组件#

react-redux 提供 Provider 组件,包裹在根组件最外层,用于传递 store 到组件内部,让应用内部的任何子孙组件非常方便地获取到全局状态,核心原理是 react 提供的 context API。
示例代码如下:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import './index.css';
import App from './App';
import { createStore } from 'redux';
import reducer from './reducers';

const store = createStore(reducer);

ReactDOM.render(
  <Provider store={store}>
    <App/>
  </Provider>,
  document.getElementById('root')
);

connect()#

react-redux 提供 connect 方法用于将 react 组件与 redux 的 store 进行连接,通过 connect 方法,可以在任何 react 组件中,将 store 中的全局 state 和 Action Creator 传递到组件的 props 中,以供组件使用。

connect 方法接受两个参数,mapStateToPropsmapDispatchToProps,用于定义组件的输入输出。mapStateToProps 负责输入逻辑,将 state 映射到组件的 props,后者负责输出逻辑,将 Action Creator 方法传递到组件的 props 中,在组件内即可发起 Action。
示例代码如下:

import { connect } from 'react-redux';

const FilterLink = connect(
  mapStateToProps,
  mapDispatchToProps
)(Link);

export default FilterLink;

mapStateToProps#

mapStateToProps 是一个函数,接受两个参数,state 和 props,state 包含整个全局对象树,props 代表组件的 props,一般情况下使用 state 情况最多。执行后返回一个对象,对象的键值对就是一个映射。
示例代码如下:

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

上述代码中,在 mapStateToProps 中,通过全局 state 和组件的 props 进行计算,得到包含键 active 的对象,传入组件,在组件中即可通过 this.props.active 来获取值。

mapDispatchToProps#

mapDispatch 是一个函数或者对象,用于将 store.dispatch 映射到组件的 props。当 mapDiapatchToProps 是一个函数时,传入 dispatch 和 ownProps 两个参数,返回一个对象,对象的键值对定义了组件的 props 传递的方法以及对应的 action。
示例代码如下:

const mapDispatchToProps = (
  dispatch,
  ownProps
) => {
  return {
    onClick: () => {
      dispatch({
        type: 'SET_VISIBILITY_FILTER',
        filter: ownProps.filter
      });
    }
  };
}

上述代码将 dispatch 方法映射到组件的 props 参数 onClick,在组件内部即可通过 this.props.onClick()进行调用。

当 mapDispatch 是一个对象时,它的键值对分别是组件的 props 参数和一个作为 Action Creator 的函数,通常在大型应用中,会在专门的文件中定义 actionCreators 文件,如下:

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

上述代码就是一系列 actionCreator 之一,返回一个 Action,由 Redux 自动发出,上述代码使用如下:

import { setVisibilityFilter } from '../actions';

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

这种方法跟写成函数的方法其实是一样的,只是将 action 进行了单独的抽离,这样可以避免更多的冗余代码,如需要在多个组件中发起相同的 action 时。

实战 To do 应用#

前面已经了解了 react-redux 的核心 API,下面来完成一个完整的应用。

整个应用实现的界面如下:

image

功能主要有,输入文字添加 todo 事项,显示 todo list,点击 todo 项切换状态,点击 tab 筛选不同的 todo 状态。

在应用开发过程中,使用 components,首先,使用 create-react-app 来初始化 react 应用,src 为主要的开发目录,src 目录下,分别新建 actions、components、containers、reducers 文件夹,actions 用于编写 actionCreator,reducers 文件夹下用于编写应用的 reducer,component 文件夹下编写 UI 组件,即无状态组件,只负责 UI 的呈现,不负责业务逻辑,数据通过 props 传入,也不操作 redux 相关的 api。containers 文件夹下编写容器组件,负责管理数据和业务逻辑,操作 redux 的 api。

首先,在App.js中,包含了整个应用的 UI 界面。

import React, { Component } from 'react';
import Footer from './components/Footer';
import AddTodo from './containers/AddTodo';
import VisibleTodoList from './containers/VisibleTodoList';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  render() {

    return (
      <div className="App">
        <div className="todoapp">
          <AddTodo/>
          <VisibleTodoList/>
          <Footer/>
        </div>
      </div>
    );
  }
}

export default App;

可以看到,整个应用包含三个组件AddTodoVisibleTodoListFooter

下面逐步介绍整个界面的实现。
从 containers 文件夹开始,首先是 AddTodo 组件,用于添加 todo。

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

AddTodo 包含一个 Form 表单,在 Form 表单的 onSubmit 事件中,获取输入值,提交至 action,整个组件被 redux 的 connect 组件包裹,从而使得 dispatch 传入到组件的 props 中。

下面是 actions 的 addTodo 方法。

let nextTodoId = 0
export const addTodo = (text) => ({
  type: 'ADD_TODO',
  id: nextTodoId++,
  text
})

addTodo 用于新建 Todo,将 todo 的 id 加 1, 传入 todo 的 text 属性。

下面是 VisibleTodoList 组件,根据当前选中的 tab 来显示不同状态下的 todo list。

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)
    default:
      throw new Error('Unknown filter: ' + filter)
  }
}

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

const mapDispatchToProps = {
  onTodoClick: toggleTodo
}

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

export default VisibleTodoList

从代码中可以看到,VisibleTodoList 没有界面相关的部分,仅仅是将 TodoList 组件进行包裹返回一个新的组件。mapStateToProps 中,根据当前所有的 todos 和筛选条件 visibilityFilter,通过 getVisibleTodos 函数,返回一个筛选后的 todos,mapDispatchToProps 则是将 actionCreator 方法通过 props 传入到 UI 组件。

在 actions 文件夹下,index.js 中,定义了 toggleTodo 如下:

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

toggleTodo 通过传入的 id 对当前操作的 todo 进行处理。

下面是 components 下的 TodoList 组件,负责显示 todo list。

import React from 'react'
import PropTypes from 'prop-types'
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

从上述代码可以看到,TodoList 接收 props 中的 todos, onTodoClick,并对 todos 进行渲染,每个 todo 又抽离成单独的 Todo 组件,在每个 Todo 上传递点击事件,点击事件中,调用 onTodoClick,并传入当前 todo 的 id,以此来改变 todo 的状态。

Todo 组件如下所示:

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

在 TodoList 组件中,渲染所有的 todo list 时,将每个 todo 进行对象解构,传入 Todo 组件的 props,在 Todo 组件中,获取到组件需要的参数 onClick, completed, text,onClick 用于绑定到 todo 的点击事件,completed 用于表示 todo 的状态是否为完成,根据 completed 的值来渲染 todo 的样式,text 渲染为 todo 的内容。
下面是 components 中的 Footer 组件:

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

Footer 组件中,用于对 todos 的状态进行筛选,共包含三个筛选项,SHOW_ALL 显示所有 todos,SHOW_ACTIVE 显示刚添加的 todo,SHOW_COMPLETED 显示已经完成的 todo。每个筛选项又抽离成 FilterLink 组件,传入 filter 作为筛选参数。

下面是 container 中 FilterLink 的代码。

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

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

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

const FilterLink = connect(
  mapStateToProps,
  mapDispatchToProps
)(Link)

export default FilterLink

FilterLink 组件也仅仅是负责数据的逻辑,没有 UI 呈现。mapStateToProps 中判断当前组件的 props.filter 与筛选条件 state.visibilityFilter 来计算 active 属性传入 props,mapDispatchToProps 定义 onClick 方法,用于 dispatch 一个 action 来改变当前的筛选条件,即 state.visibilityFilter,传入当前组件的 filter 属性作为参数。

actions 的 setVisibilityFilter 代码如下:

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

下面是 Link 组件。

import React from 'react'
import PropTypes from 'prop-types'

const Link = ({ active, children, onClick }) => {
  if (active) {
    return <span>{children}</span>
  }

  return (
    // eslint-disable-next-line
    <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

Link 组件通过 props 接收 active,children,onClick 属性值,当 active 为 true 时,Link 渲染为 span,否则渲染为 a,a 标签绑定点击事件,点击时,触发 props 中的 onClick 方法。

自此,界面部分的代码都已经介绍完毕,下面是 reducer 的编写。
在 reducer 的编写中,为了让 reducer 的职责更为清晰,将 reducer 拆分为 todos 和 visibilityFilter,todos.js 中,用于处理 todo 的添加,状态改变。代码如下:

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

此处,todos 初始化为一个空数组,reducer 必须为纯函数,处理 actions,不能直接对 state 进行修改,应该每次都返回一个新的 state,当添加 Todo 时,使用数组展开运算符对 state 进行解构,并且,将新的 todo 对象作为数组的最后一个值,最终,生成一个全新的 state。当修改 todo 状态时,使用数组的 map 方法,同样返回一个新的 state,在 map 方法内部,使用三元运算符对 action 参数 id 和数组每一项的 id 进行判断,目的是寻找到当前被点击时的 todo,将其 completed 值取反,否则直接返回当前 todo。

visibilityFilter.js 中,主要是用于更改 todo list 的筛选条件,代码如下:

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

export default visibilityFilter

此时,state 初始化为 SHOW_ALL,即应用加载时,默认显示为全部的 todos,当处理 actions,直接返回 action.filter。

当 reducer 被拆分为单个文件时,上述 const 命名的函数名即为全局状态树中的 state 值,还需要将拆分的 reducer 进行组合,组合 reducer 主要使用了 redux 的 combineReducers API,以下为 reducers 文件夹下 index.js 代码:

import { combineReducers } from 'redux'
import todos from './todos'
import visibilityFilter from './visibilityFilter'

const todoApp = combineReducers({
  todos,
  visibilityFilter
})

export default todoApp

最后一步,需要使用 react-redux 的 Provider API 包裹整个应用,src 下 index.js 中:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'
import './index.css';
import App from './App';
import { createStore } from 'redux';
import reducer from './reducers';

const store = createStore(reducer);

ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById('root')
);

浏览器运行效果如下:

image

至此,整个 To do 应用就开发完毕了,对比使用 redux 开发,react-redux 让我们开发 react 应用的时候更加简单。

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。