Redux入门实战——todo-list2.0实现

时间:2022-07-23
本文章向大家介绍Redux入门实战——todo-list2.0实现,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

1.前言

在之前的博客中,我写了一篇关于todo-list实现的博客,一步一步详细的记录了如何使用基础的React知识实现一个React单页面应用,通过该篇文章,能够对React入门开发有一个直观的认识和粗浅的理解。

近期,个人学习了一下Redux,又将该项目使用 React+Redux的方式进行了实现。本片内容记录以下实践的过程。通过本实例,可以学习到:

  • Redux的核心思想;
  • Redux的三大概念;
  • React+Redux的开发方法和流程;

下面将从以下几个方面展开讲解和记录。

2.项目演示

3.Redux基础知识

3.1 认识

3.1.1 动机

随着 JavaScript 单页面应用开发日趋复杂,JavaScript 需要管理比任何时候都要多的 state (状态),管理不断变化的 state 非常困难,state 在什么时候,由于什么原因,如何变化已然不受控制。当系统变得错综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。

因此,需要一种更可控的方式来管理系统的state,让系统的state变得可预测,redux就是用来管理系统state的工具。

3.1.2 三大原则

  • 单一数据源 整个应用的状态都保存在一个对象中,一个应用只有一个唯一的state,保存在store中,通过store统一管理。
  • 状态是只读的 唯一改变 state 的方法就是触发 actionaction 是一个用于描述已发生事件的普通对象。 redux不会直接修改state,而是在状态发生更改时,返回一个全新的状态,旧的状态并没有进行更改,得以保留。可以使用 redux-devtools-extension 工具进行可视化查看。
  • 状态修改由纯函数完成 Reducer 只是一些纯函数,它接收先前的 state 和 action,并返回新的 state。

3.2 基础

3.2.1 Store

Redux的核心是 Store ,StorecreateStore方法创建,

createStore(reducer, [initState])//reducer表示一个根reducer,initState是一个初始化状态

store提供方法来操作state

3.2.2 Action

action 是把数据从应用传到 store 的有效载荷。它是 store 数据的唯一来源。通过 store.dispatch() 将 action 传到 store。如果有数据需要添加,在action中一并传过来。

action需要action创建函数进行创建,如下是一个action创建函数:

/*
 * action 类型
 */

export const ADD_TODO = 'ADD_TODO';
export const TOGGLE_TODO = 'TOGGLE_TODO'
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'

/*
 * 其它的常量
 */

export const VisibilityFilters = {
  SHOW_ALL: 'SHOW_ALL',
  SHOW_COMPLETED: 'SHOW_COMPLETED',
  SHOW_ACTIVE: 'SHOW_ACTIVE'
}

/*
 * action 创建函数
 */

export function addTodo(text) {
  return { type: ADD_TODO, text }
}

export function toggleTodo(index) {
  return { type: TOGGLE_TODO, index }
}

export function setVisibilityFilter(filter) {
  return { type: SET_VISIBILITY_FILTER, filter }
}

返回一个对象,改对象由reducer获取,根据 action 类型进行相应操作。

3.2.3 Reducer

store通过 store.dispatch(某action(参数)) 来给reducer安排任务。

简单理解,一个reducer就是一个函数,这个函数接受两个参数 当前stateaction,然后根据 action 来对当前 state 进行操作,如果有需要更改的地方,就返回一个 新的 state ,而不会对旧的 state进行操作,任何一个阶段的 state 都可以进行查看和监测,这让 state 的管理变得可控,可以实时追踪 state的变化。

React中使用Redux时,需要有一个根 Reducer,这个根 Reducer 通过 conbineReducer() 将多个子 Reducer 组合起来。

根reducer:

import { combineReducers } from 'redux'
import todos from './todos'
import visibilityFilter from './visibilityFilter'
//根reducer
// rootReducer 根reducer,把子reducer组合在一起
export default combineReducers({
  todos, //子state
  visibilityFilter //子state
})

子reducer:

//这里的state = []为state的当前值
const todos = (state = [], action) => {
    switch (action.type) {
      case 'ADD_TODO':
        return [
          ...state,     // Object.assign() 新建了一个副本
          {
            id: action.id,
            text: action.text,
            completed: false
          }
        ]
      case 'TOGGLE_TODO':
     //   console.log(state);
        return state.map((value,index) => {
            return (value.id === action.id) ? {...value,completed:!value.completed} : value;
        }) 
      default:
        return state;
    }
  }

export default todos;

3.2.4 数据流

3.3 展示组件和容器组件

3.3.1 展示组件和容器组件分离

本部分在笔者尚未深入研究,在此给出redux作者写的深度解析文章链接及网上的译文链接,读者可自行查看。

原文链接:展示组件和容器组件相分离 译文链接:展示组件和容器组件相分离

3.3.2 展示组件和容器组件比较

展示组件

容器组件

作用

描述如何展示骨架、样式

描述如何运行(数据获取、状态更新)

直接使用Redux

数据来源

props

监听Redux state

数据修改

从props调用回调函数

向Redux派发action

调用方式

手动

通常由React Redux生成

大部分的组件都应该是展示型的,但一般需要少数的几个容器组件把它们和 Redux store 连接起来。

React Redux 的使用 connect() 方法来生成容器组件。

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

//mapStateToProps参数中的state是store的state.
// 在容器组件中,通过mapStateToProps方法,在展示组件和store中间传递数据和执行action
// ownProps表示的是组件自身的属性,即父组件传过来的属性
const mapStateToProps = (state, ownProps) => {
    return {
        active: ownProps.filter === state.setVisibilityFilter
    }
}
// ownProps表示的是组件自身的属性,即父组件传过来的属性
const mapDispatchToProps = (dispatch, ownProps) => {
    return {
        // 这里写方法名,在展示组件中通过这个方法名来执行里面的action派遣函数
        onClick: () => {
            // 执行setVisibilityFilter这个action
            dispatch(setVisibilityFilter(ownProps.filter))
        }
    }
}
//通过connect让Link组件得以连接store,从store中取得active数据和onClick方法的执行体。
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Link)

connect() 中最核心的两个方法是:mapActionToPropsmapDispatchToProps ,通过容器组件,可以在 展示组件和 store之间传递数据和执行 action

4.基于Redux的React项目实战

4.1 目录结构

根据Redux的几大组成部分,在进行开发时,将在之前基础的React开发模式下,增加几个文件夹,形成新的开发目录结构,具体目录结构如下图:

│  App.css
│  App.js
│  App.test.js
│  index.css
│  index.js
│  logo.svg
│  readme.txt
│  serviceWorker.js
│  setupTests.js
├─actions      
├─components       
├─containers
└─reducers

如图,在之前的结构下,新增了 actionsreducerscontainers 这三个文件夹。

4.2 配置React-Redux开发环境

4.2.1 步骤

在建好文件目录后就可以开始进行开发了,由于是基于Redux做React开发,所以首先一步当然需要把Redux的开发环境配置一下。

  • 安装 react-redux
npm install --save react-redux
  • 编写入口文件 index.js

前文讲到,redux使用一个唯一的 store 来对项目进行状态管理,那么首先我们需要创建这个 store ,并将这个 store 作为一个属性,传递给下级子组件。

具体代码如下:

import React from 'react';
import ReactDOM, { render } from 'react-dom';

//redux ----------------------------------------------------
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import { rootReducer } from './reducers';
//引入项目根组件App.jsx
import App from './App';

//创建store,将根Reducer传入store中。redux应用只有一个单一的store
const store = createStore(rootReducer);

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

如上代码所示,使用Redux,需要引入的文件有:

  • Provider 组件
  • createStore 方法
  • 根reducer
  • 项目根组件App.jsx

createStorecreateStore 方法可接受两个参数,第一个是项目的根 reducer ,是必选的参数,另一个是可选的参数,可输入项目的初始 state 值。通过该方法创建一个 store 实例,即为项目唯一的 store

Provider组件Provider组件包裹在跟组件App.jsx外层,将项目的 store作为属性传递给 Provider。使用Provider 可以实现所有子组件直接对 store 进行访问。在下文将深入讲一下 Provider 的实现和工作原理。

根reducer:随之项目的不断增大,程序state的越来越复杂,只用一个 reducer 是很难满足实际需求的,redux中采用将 reducer 进行拆分,最终在状态改变之前通过 根 reducer 将 各个拆分的子 reducer 进行合并方式来进行处理。

App.jsx:项目的跟组件,将一级子组件写在App.jsx中。

4.2.2 Provider

provider 包裹在根组件外层,使所有的子组件都可以拿到state。它接受store作为props,然后通过context往下传,这样react中任何组件都可以通过context获取store。

Provider 原理:

原理是React组件的context属性

组件源码如下:

原理是React组件的context属性

export default class Provider extends Component {
  getChildContext() {
      //返回一个对象,这个对象就是context
    return { store: this.store }
  }

  constructor(props, context) {
    super(props, context)
    this.store = props.store
  }
  render() {
    return Children.only(this.props.children)
  }
}

Provider.propTypes = {
  store: storeShape.isRequired,
  children: PropTypes.element.isRequired
}

Provider.childContextTypes = {
  store: storeShape.isRequired
}

4.3 src目录文件列表

文件夹

文件

src

index.js

src/actions

index.js

src/components(展示组件)

App.jsx

TodoList.jsx

Footer.jsx

Todo.jsx

Link.jsx

src/containers(容器组件)

AddTodo.js

FilterLink.js

VisibleTodoList.js

src/reducers

index.js

todo.jsx

visibilityFilter.js

4.4 项目代码

注意:

  • 代码说明大部分写在项目代码中,读者在查看时,建议对代码也要进行仔细阅读。
  • 本项目功能较简单,因此代码直接按照文件目录给出,而不按照功能模块陈列。

4.4.1 入口文件 index.js

import React from 'react';
import ReactDOM, { render } from 'react-dom';
import './index.css';
import App from './components/App';

//redux
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import rootReducer from './reducers';

//创建store,createStore()第一个参数是项目的根reducer,第二个参数是可选的,用于设置state的初始状态
const store = createStore(rootReducer);

render(
  // Provider组件包裹在跟组件的外层,使所有的子组件都可以拿到state.
  // 它接受store作为props,然后通过context往下传,这样react中任何组件
  // 都可以通过context获取store.
  <Provider store = {store}>
    {/* App 根组件 */}
    <App />
  </Provider>,
  document.getElementById('root')
)

4.4.2 actions文件

  • index.js
let nextTodoId = 0;

// 定义action 常量 对于小型项目,可以将action常量和action创建函数写在一起,对于复杂的项目,可将action常量和其他的常量抽取出来,放到单独的某个常量文件夹中
const ADD_TODO = 'ADD_TODO';
const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER';
const TOGGLE_TODO = 'TOGGLE_TODO';

//这里是几个action创建函数,函数里面的对象才是action,返回一个action
// text是跟随action传递的数据
// 调用 dispatch(addTodo(text)),即代表派遣action,交给reducer处理
//action生成函数
// 大部分情况下,他简单的从参数中收集信息,组装成一个action对象并返回,
// 但对于较为复杂的行为,他往往会容纳较多的业务逻辑与副作用,包括与后端的交互等等。
export const addTodo = (text) => {
    return {
        type: ADD_TODO,
        id: nextTodoId ++,
        text
    }
}
export const setVisibilityFilter = (filter) => {
    return {
        type: SET_VISIBILITY_FILTER,
        filter
    }
}
export const toggleTodo = (id) => {
    return {
        type: TOGGLE_TODO,
        id
    }
}
//三个常量
export const VisibilityFilters = {
  SHOW_ALL: 'SHOW_ALL',
  SHOW_COMPLETED: 'SHOW_COMPLETED',
  SHOW_ACTIVE: 'SHOW_ACTIVE'
}

4.4.3 components文件(展示组件)

  • App.jsx
import React from 'react'
import Footer from './Footer'
import AddTodo from '../containers/AddTodo'
import VisibleTodoList from '../containers/VisibleTodoList'
//应用的根组件
const App = () => {
  return (
    <div>
      {/* 容器组件 */}
      <AddTodo />
      {/* 容器组件 */}
      <VisibleTodoList />
      {/* 展示组件 */}
      <Footer />
    </div>
  )  
}
export default App
  • Footer.jsx
import React from 'react'
import FilterLink from '../containers/FilterLink'
import { VisibilityFilters } from '../actions'
//无状态组件,这种写法初学者可能难以理解,可以先补习下ES6,等价于
//function Footer(){
//	return (<div>XXX</div>)
//}
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.jsx
import React from 'react'
import PropTypes from 'prop-types'
//prop-types是一个组件属性校验包,导入这个包可以数据进行格式等方面的校验
const Link = (props) => {
    return (
     <button onClick={props.onClick} disabled={props.active} style={{marginLeft:'4px'}}>
       {props.children}
     </button>
    )
}

Link.propTypes = {
  active: PropTypes.bool.isRequired,
  children: PropTypes.node.isRequired,
  onClick: PropTypes.func.isRequired
}

export default Link
  • TodoList.jsx
import React, { createFactory } from 'react'
import PropTypes from 'prop-types'
import Todo from './Todo'

const TodoList = (props) => {
    return (
        <ul>
            {
                props.todos.map((value,index) => {
                    return <Todo key = {index} {...value} onClick = {() => props.toggleTodo(value.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
  • Todo.jsx
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

4.4.4 containers文件(容器组件)

注意:本部分涉及 connect() 方法,代码注释中有重要知识点,建议仔细查看。对于connect()本文不做深入探讨,后续会单独成文分析。

  • FilterLink.js
import { connect } from 'react-redux'
import { setVisibilityFilter } from '../actions'
import Link from '../components/Link'
import { createFactory } from 'react'

//mapStateToProps参数中的state是store的state.
// 在容器组件中,通过mapStateToProps方法,在展示组件和store中间传递数据和执行action
// ownProps表示的是组件自身的属性,即父组件传过来的属性
const mapStateToProps = (state, ownProps) => {
    return {
        active: ownProps.filter === state.setVisibilityFilter
    }
}

// ownProps表示的是组件自身的属性,即父组件传过来的属性
const mapDispatchToProps = (dispatch, ownProps) => {
    return {
        // 这里写方法名,在展示组件中通过这个方法名来执行里面的action派遣函数
        onClick: () => {
            // 执行setVisibilityFilter这个action
            dispatch(setVisibilityFilter(ownProps.filter))
        }
    }
}

//通过connect让Link组件得以连接store,从store中取得active数据和onClick方法的执行体。
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Link)

// //将Link组件的内容放到本页面来结合起来理解,以下代码不是本组件的功能代码
// const Link = ({ active, children, onClick }) => (
//     <button
//        onClick={onClick}
//        disabled={active}
//        style={{
//            marginLeft: '4px',
//        }}
//     >
//       {children}
//     </button>
//   )
  
//   Link.propTypes = {
//     active: PropTypes.bool.isRequired,
//     children: PropTypes.node.isRequired,
//     onClick: PropTypes.func.isRequired
//   }

建议将容器组件和它对应的展示组件紧密结合起来理解。

  • AddTodo.js
import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../actions'

const 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>
  )
}

export default connect()(AddTodo);
  • VisibleTodoList.js
import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'

//获取符合条件的todo,
// todos state中的todo数据
// filter state中的过滤条件
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 {
        toggleTodo: (id) => {
            dispatch(toggleTodo(id))
        }
    }
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

4.4.5 reducer文件夹

  • 根reducer/index.js
import { combineReducers } from 'redux'
import todos from './todos'
import visibilityFilter from './visibilityFilter'
// rootReducer 根reducer,把子reducer组合在一起
export default combineReducers({
  todos, //子state
  visibilityFilter //子state
})
  • todo.js
//这里的state = []为state的当前值
const todos = (state = [], action) => {
    switch (action.type) {
      case 'ADD_TODO':
        return [
          ...state,     // Object.assign() 新建了一个副本
          {
            id: action.id,
            text: action.text,
            completed: false
          }
        ]
      case 'TOGGLE_TODO':
     //   console.log(state);
        return state.map((value,index) => {
            return (value.id === action.id) ? {...value,completed:!value.completed} : value;
        }) 
      default:
        return state;
    }
  }

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

5.总结

本文,菜鸡本鸡通过一个todo-list实例相对系统的介绍了redux的一些基础概念,基本用法和如何如react进行结合,实现react的功能开发,主要内容包括redux基础,redux于react结合,实例完成步骤,完整代码,项目演示等,比较适合刚接触redux的菜鸟阅读和学习,希望能帮助到有需要的同学。