Redux入门实战——todo

Stella981
• 阅读 805

1.前言

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

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

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

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

2.项目演示

Redux入门实战——todo

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 数据流

Redux入门实战——todo

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

Redux入门实战——todo

如图,在之前的结构下,新增了 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 (

    {/* 容器组件 /} {/ 容器组件 /} {/ 展示组件 */}
    )
    } export default App

  • Footer.jsx

    import React from 'react' import FilterLink from '../containers/FilterLink' import { VisibilityFilters } from '../actions' //无状态组件,这种写法初学者可能难以理解,可以先补习下ES6,等价于 //function Footer(){ // return (

    XXX
    ) //} const Footer = () => (

    Show: All Active Completed
    ) 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} ) }

    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 (

      { props.todos.map((value,index) => { return <Todo key = {index} {...value} onClick = {() => props.toggleTodo(value.id)} /> }) }
    ) }

    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 }) => (

  • {text}
  • )

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

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

    <form onSubmit={e => { e.preventDefault() if (!input.value.trim()) { return } dispatch(addTodo(input.value)) input.value = '' }} > <input ref={node => input = node} />
    ) }

    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的菜鸟阅读和学习,希望能帮助到有需要的同学。

6 参考资料

本文同步分享在 博客“CherishTheYouth”(CNBlog)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
java将前端的json数组字符串转换为列表
记录下在前端通过ajax提交了一个json数组的字符串,在后端如何转换为列表。前端数据转化与请求varcontracts{id:'1',name:'yanggb合同1'},{id:'2',name:'yanggb合同2'},{id:'3',name:'yang
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这