Redux入门实战——todo

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 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。

    redux不会直接修改state,而是在状态发生更改时,返回一个全新的状态,旧的状态并没有进行更改,得以保留。可以使用 redux-devtools-extension 工具进行可视化查看。

  • 状态修改由纯函数完成

    Reducer 只是一些纯函数,它接收先前的 state 和 action,并返回新的 state。

3.2 基础

3.2.1 Store

Redux的核心是 Store ,Store 由 createStore方法创建,

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

store提供方法来操作state

3.2.2 Action

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

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

1/* 2 * action 类型 3 */ 4 5export const ADD_TODO = 'ADD_TODO'; 6export const TOGGLE_TODO = 'TOGGLE_TODO' 7export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER' 8 9/* 10 * 其它的常量 11 */ 12 13export const VisibilityFilters = { 14 SHOW_ALL: 'SHOW_ALL', 15 SHOW_COMPLETED: 'SHOW_COMPLETED', 16 SHOW_ACTIVE: 'SHOW_ACTIVE' 17} 18 19/* 20 * action 创建函数 21 */ 22 23export function addTodo(text) { 24 return { type: ADD_TODO, text } 25} 26 27export function toggleTodo(index) { 28 return { type: TOGGLE_TODO, index } 29} 30 31export function setVisibilityFilter(filter) { 32 return { type: SET_VISIBILITY_FILTER, filter } 33}

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

3.2.3 Reducer

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

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

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

根reducer:

1import { combineReducers } from 'redux' 2import todos from './todos' 3import visibilityFilter from './visibilityFilter' 4//根reducer 5// rootReducer 根reducer,把子reducer组合在一起 6export default combineReducers({ 7 todos, //子state 8 visibilityFilter //子state 9})

子reducer:

1//这里的state = []为state的当前值 2const todos = (state = [], action) => { 3 switch (action.type) { 4 case 'ADD_TODO': 5 return [ 6 ...state, // Object.assign() 新建了一个副本 7 { 8 id: action.id, 9 text: action.text, 10 completed: false 11 } 12 ] 13 case 'TOGGLE_TODO': 14 // console.log(state); 15 return state.map((value,index) => { 16 return (value.id === action.id) ? {...value,completed:!value.completed} : value; 17 }) 18 default: 19 return state; 20 } 21 } 22 23export 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() 方法来生成容器组件。

1import { connect } from 'react-redux' 2import { setVisibilityFilter } from '../actions' 3import Link from '../components/Link' 4 5//mapStateToProps参数中的state是store的state. 6// 在容器组件中,通过mapStateToProps方法,在展示组件和store中间传递数据和执行action 7// ownProps表示的是组件自身的属性,即父组件传过来的属性 8const mapStateToProps = (state, ownProps) => { 9 return { 10 active: ownProps.filter === state.setVisibilityFilter 11 } 12} 13// ownProps表示的是组件自身的属性,即父组件传过来的属性 14const mapDispatchToProps = (dispatch, ownProps) => { 15 return { 16 // 这里写方法名,在展示组件中通过这个方法名来执行里面的action派遣函数 17 onClick: () => { 18 // 执行setVisibilityFilter这个action 19 dispatch(setVisibilityFilter(ownProps.filter)) 20 } 21 } 22} 23//通过connect让Link组件得以连接store,从store中取得active数据和onClick方法的执行体。 24export default connect( 25 mapStateToProps, 26 mapDispatchToProps 27)(Link)

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

4.基于Redux的React项目实战

4.1 目录结构

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

1│ App.css 2│ App.js 3│ App.test.js 4│ index.css 5│ index.js 6│ logo.svg 7│ readme.txt 8│ serviceWorker.js 9│ setupTests.js 10├─actions 11├─components 12├─containers 13└─reducers

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

4.2 配置React-Redux开发环境

4.2.1 步骤

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

  • 安装 react-redux 包

    npm install --save react-redux

  • 编写入口文件 index.js

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

具体代码如下:

1import React from 'react'; 2import ReactDOM, { render } from 'react-dom'; 3 4//redux ---------------------------------------------------- 5import { Provider } from 'react-redux'; 6import { createStore } from 'redux'; 7import { rootReducer } from './reducers'; 8//引入项目根组件App.jsx 9import App from './App'; 10 11//创建store,将根Reducer传入store中。redux应用只有一个单一的store 12const store = createStore(rootReducer); 13 14render( 15 <Provider store = {store}> 16 <App /> 17 </Provider>, 18 document.getElementById('id') 19) 20

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

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

createStore:createStore 方法可接受两个参数,第一个是项目的根 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属性

1export default class Provider extends Component { 2 getChildContext() { 3 //返回一个对象,这个对象就是context 4 return { store: this.store } 5 } 6 7 constructor(props, context) { 8 super(props, context) 9 this.store = props.store 10 } 11 render() { 12 return Children.only(this.props.children) 13 } 14} 15 16Provider.propTypes = { 17 store: storeShape.isRequired, 18 children: PropTypes.element.isRequired 19} 20 21Provider.childContextTypes = { 22 store: storeShape.isRequired 23}

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

1import React from 'react'; 2import ReactDOM, { render } from 'react-dom'; 3import './index.css'; 4import App from './components/App'; 5 6//redux 7import { Provider } from 'react-redux'; 8import { createStore } from 'redux'; 9import rootReducer from './reducers'; 10 11//创建store,createStore()第一个参数是项目的根reducer,第二个参数是可选的,用于设置state的初始状态 12const store = createStore(rootReducer); 13 14render( 15 // Provider组件包裹在跟组件的外层,使所有的子组件都可以拿到state. 16 // 它接受store作为props,然后通过context往下传,这样react中任何组件 17 // 都可以通过context获取store. 18 <Provider store = {store}> 19 {/* App 根组件 */} 20 <App /> 21 </Provider>, 22 document.getElementById('root') 23)

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

6 参考资料

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

点赞
收藏

评论区

加载中...

相关推荐

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

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

java将前端的json数组字符串转换为列表

记录下在前端通过ajax提交了一个json数组的字符串,在后端如何转换为列表。前端数据转化与请求varcontracts{id:'1',name:'yanggb合同1'},{id:'2',name:'yanggb合同2'},{id:'3',name:'yang