前端筆記之React(五)Redux深入淺出

mufengsm發表於2019-08-02

一、Redux整體感知

ReduxJavaScript狀態管理容器,提供了可被預測狀態的狀態管理容器。來自於Flux思想,Facebook基於Flux思想,在2015年推出Redux庫。

中文網站:http://www.redux.org.cn/

官方githttps://github.com/reduxjs/redux

 

首先要引redux.js包,這個包提供了Redux物件,這個物件可以呼叫Redux.createStore()方法。

<body>
    <h1 id="info"></h1>
    <button id="btn1">加</button>
    <button id="btn2">減</button>
    <button id="btn3">乘</button>
    <button id="btn4">除</button>

    <script type="text/javascript" src="redux.min.js"></script>
    <script type="text/javascript">
        //這是一個 reducer,形式為 (state, action) => state 的純函式。
        //純函式:函式內部,不改變傳入的引數,只return新值。
        //描述了 action 如何把 state 轉變成下一個 state。
        const reducer = (state = {"v" : 10} , action)=>{
            if(action.type == "ADD"){
                return {"v" : state.v + 1};
            }else if(action.type == "MINUS"){
                return {"v" : state.v - 1};
            }else if(action.type == "CHENG2"){
                return {"v" : state.v * 2}
            }
             return state;
        }
        //建立 Redux store 來存放應用的狀態。
        //store翻譯為“倉庫”,這是一個存放資料並且可以運算元據的東西
        const store = Redux.createStore(reducer);

        //建立一個檢視函式,並且將store顯示到檢視上。
        const render = ()=>{
            //store的getState()方法可以得到倉庫中的資料
            document.getElementById("info").innerHTML = store.getState().v;
        }
        render(); //呼叫render函式
        //要將store註冊到檢視,這樣的話,當store中的資料變化候,就能自動呼叫render函式
        store.subscribe(render);

        // 應用中所有的 state 都以一個物件樹的形式儲存在一個單一的 store 中。
        // 惟一改變state的辦法是觸發action,一個描述發生什麼的物件。
        // 為了描述 action 如何改變 state 樹,你需要編寫 reducers。
        document.getElementById("btn1").onclick = function(){
            // 改變內部 state 惟一方法是 dispatch 一個 action,type表示要做的動作
            store.dispatch({"type":"ADD"})
        }
        document.getElementById("btn2").onclick = function(){
            store.dispatch({"type":"MINUS"})
        }
        document.getElementById("btn3").onclick = function(){
            store.dispatch({"type":"CHENG"})
        }
    </script>
</body>

建立一個叫做reducer的函式,reducer中“維持”一個量,叫state,初始值是{"v" : 0}

 

這個函式你必須知道兩點:

1)它是一個純函式,在函式內部不改變{"v" : 0}的,不改變state物件的,只是返回了新的state

const reducer = (state = {"v" : 0} , action)=>{
    if(action.type == "ADD"){
        return {"v" : state.v + 1};
    }else if(action.type == "MINUS"){
        return {"v" : state.v - 1};
    }
    return state;
}

 

2)這個函式提供了可被預測的功能。

reducer中的state,就像被關進了保險箱。任何對這個state的變化,只能是通過dispatch一個action來改變的。換句話說,只有dispatch一個action才能改變這個保險箱中的資料

reducer函式中,用if語句來表示對state可能發生變化的羅列,只有羅列在樓層裡面的改變,才會發生:

if(action.type==""){
    return 新的state
}else if(action.type ==""){
    return 新的state
}else if(action.type ==""){
    return 新的state
}
示例程式碼

建立store倉庫,這個倉庫建立時需要提供reducer,所以可以認為store就是reducerreducer就是store

const store = Redux.createStore(reducer);

reducer是一個純函式,而store提供了三個方法:

 store.subscribe() 註冊到檢視

 store.getState() 得到資料

 store.dispatch() 傳送action

 

學習reduxreact結合,到時候就不用註冊到檢視。

然後建立監聽:

document.getElementById("btn1").onclick = function(){
     store.dispatch({"type":"ADD"})
}
document.getElementById("btn2").onclick = function(){
     store.dispatch({"type":"MINUS"})
}
document.getElementById("btn3").onclick = function(){
     store.dispatch({"type":"CHENG"})
}
示例程式碼

點選按鈕,storedispatch出一個action。所謂的action就是一個JSON,這個JSON必須有type屬性,值為大寫字母。這個action沒有任何意義,比如{"type":"ADD"},但reducer認識這個action,可以產生變化!

 

案例2,新增載荷:

<body>
    <h1 id="info"></h1>
    <button id="btn1">加</button>

    <input type="text" id="txt">
<button id="btn2">加輸入的</button>

    <script type="text/javascript" src="redux.min.js"></script>
    <script type="text/javascript">
        const reducer = (state = {"v" : 10}, {type, data})=>{
            if(type == "ADD"){
            return {"v" : state.v + action.data}
          return {"v" : state.v + data}
            }
            return state;
        }
        const store = Redux.createStore(reducer);
        const render = ()=>{
            document.getElementById("info").innerHTML = store.getState().v;
        }
        render();
        store.subscribe(render);

    document.getElementById("btn1").onclick = function(){
        store.dispatch({ type: 'ADD', data:100});
    }

        document.getElementById("btn2").onclick = function(){
            var a = parseInt(document.getElementById("txt").value);
            //載荷payload。
            store.dispatch({"type":"ADD" , data:a});
        }
    </script>
</body>

唯一可以改變state的方式dispatch一個action

 

案例3:新增陣列

<body>
    <div id="box">
        <p><input type="text" id="name"></p>
        <p><input type="text" id="age"></p>
        <button id="btn">增加</button>
        <ul id="List">

        </ul>
    </div>

    <script type="text/javascript" src="redux.min.js"></script>
    <script type="text/javascript">
        //初始資料
        const initObj = {
            "arr" : [
                {"name" : "小明" , "age" : 12},
                {"name" : "小紅" , "age" : 13},
                {"name" : "小強" , "age" : 14}
            ]
        };
        //reducer函式
        const reducer = (state = initObj, {type , name , age}) => {
            if(type == "ADD_STUDENT"){
                //不能push改變傳入的原陣列,所以要返回新陣列
                return {
                    "arr" : [
                        {name , age} ,
                        ...state.arr
                    ]
                }
            }
            return state;
        }
        //建立store
        const store = Redux.createStore(reducer);
        //檢視函式
        const render = function(){
            //清空ul
            document.getElementById("List").innerHTML = "";
            //建立li
            for(var i = 0 ; i < store.getState().arr.length ; i++){
                var li = document.createElement("li");
            var storeArr = store.getState().arr[i]
                li.innerHTML = storeArr.name + storeArr.age+"歲"
                document.getElementById("List").appendChild(li);
            }
        }
        render();//執行檢視函式
        store.subscribe(render);//註冊到檢視
        
        document.getElementById("btn").onclick = function(){
            var name = document.getElementById("name").value;
            var age = document.getElementById("age").value;
            //發出Action,這是唯一能夠改變store的途徑
            store.dispatch({"type" : "ADD_STUDENT", name, age })
        }
    </script>
</body>

 

案例4,加深練習:

<body>
    <h1 id="info"></h1>
    <h1 id="info2"></h1>
    <button id="btn">+</button>

    <script type="text/javascript" src="redux.min.js"></script>
    <script type="text/javascript">
        var initObj = {
            "a" : {
                "b" : {
                    "c" : {
                        "v" : 10
                    }
                },
                "m" : 8
            }
        }
        const reducer = (state = initObj , action) => {
            if(action.type == "ADD"){
                return {
                    ...state, 
                    "a" : {
                        ...state.a ,
                        "b" : {
                            ...state.a.b,
                            "c" : {
                                ...state.a.b.c ,
                                "v" : state.a.b.c.v + 1
                            }
                        }
                    }
                }
            }
            return state;
        }
        const store = Redux.createStore(reducer);
        const render = ()=>{
            document.getElementById("info").innerHTML = store.getState().a.b.c.v;
            document.getElementById("info2").innerHTML = store.getState().a.m;
        }
        render();
        store.subscribe(render);

        document.getElementById("btn").onclick = function(){
            store.dispatch({"type" : "ADD"});
        }
    </script>
</body>

二、ReduxReact進行結合開發

React開發的時候使用Redux可預測狀態容器,要裝兩個新的依賴:

 redux:提供createStorecombineReducersbindActionCreators等功能

 react-redux:只提供兩個東西,<Provider>元件、connect()函式。

 

安裝兩個依賴:

npm install --save redux react-redux

 

建立reducers資料夾,建立index.js,這個檔案暴露一個純函式:

reducers/index.js

export default (state = { v : 10}, action) => {
    return state;
}

 

main.js入口檔案:

import React from "react";
import ReactDOM from "react-dom";
import {createStore} from "redux";
import App from "./App.js";
import reducer from "./reducers";  //reducer函式

//建立 Redux store 來存放應用的狀態。
const store = createStore(reducer);

ReactDOM.render(
    <App></App>,
    document.getElementById("app")
);

有兩個問題:

如何讓元件能夠訪問到store中的資料?

如果讓store中的資料改變的時候,能夠自動更新元件的檢視

 

react-redux解決了這兩個問題。

main.js中建立<Provider>元件,它提供的是一個頂層容器的作用,實現store的上下文傳遞,是讓store能夠“注入”到所有元件

react-redux提供Provider元件,可以讓容器元件拿到state

Provider在根元件外面包一層,這樣一來,App所有的子元件就預設都拿到了state了。

原理是React元件的context屬性,就是將store這個物件放到上下文(context)中

import React from "react";
import ReactDOM from "react-dom";
import {createStore} from "redux";
import {Provider} from "react-redux";
import App from "./App.js";
import reducer from "./reducers";

//建立store倉庫
const store = createStore(reducer);

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

元件中要獲得全域性store的值,需要用connect函式,connect提供連線React元件與Redux store的作用。

connect([mapStateToProps], [mapDispatchToProps])

connect()的第一個引數:mapStateToProps這個函式的第一個引數就是Reduxstore,會自動將store的資料作為props繫結到元件上。

connect()的第二個引數:mapDispatchToProps它的功能是,將action作為props繫結到元件上

 

通俗理解,使用connect可以把statedispatch繫結到React元件,使得元件可以訪問到redux的資料。

常看到下面這種寫法:

export default connect()(App)

App.js

import React from "react";
import {connect} from "react-redux";
class App extends React.Component {
    constructor() {
        super();
     
    }
    render(){
        return <div>
            <h1>{this.props.v}</h1>
        </div>
    }
}
export default connect(
//這個函式return的物件的值,將自動成為元件的props。
    (state) => {
        return {
            v : state.v
        }
    }
)(App);

一旦connect()(App); 連線某個元件,此時這個元件就會:當全域性store傳送改變時,如同自己的props發生改變一樣,從而進行檢視更新。

 

App.js寫兩個按鈕,可以加1,減1

import React from "react";
import {connect} from "react-redux";
class App extends React.Component {
    constructor() {
        super();
    }
    render(){
        return <div>
            <h1>{this.props.v}</h1>
            <button onClick={()=>{this.props.add()}}>+</button>
            <button onClick={()=>{this.props.minus()}}>-</button>
        </div>
    }
}
export default connect(
    (state) => {
        return {
            v : state.v
        }
    },
    (dispatch) => {
        return {
            add(){
                dispatch({"type" : "ADD"});
            },
            minus(){
                dispatch({"type" : "MINUS"});
            }
        }
    }
)(App);

reducers/index.js提供可預測狀態

export default (state = {"v" : 10} , action) => {
    if(action.type == "ADD"){
        return { "v" : state.v + 1 };
    }else if(action.type == "MINUS"){
        return { "v" : state.v - 1 };
    }
    return state;
}

 

學生管理系統小案例:

reducers/index.js

const initObj = {
    "arr" : [
        {"id" : 1, "name" : "小明" , "age" : 12},
        {"id" : 2, "name" : "小紅" , "age" : 13},
        {"id" : 3, "name" : "小剛" , "age" : 14}  
    ]
}
export default (state = initObj, {type, age, name, id})=>{
    if(type == "ADD_STUDENT"){
        return {
            ...state , 
            "arr" : [
                ...state.arr ,
                {
                    "id" : state.arr.reduce((a,b)=>{
                        return b.id > a ? b.id : a;
                    },0) + 1,
                    name, 
                    age
                }
            ]
        }
    }else if(type == "DEL_STUDENT"){
        return {
            ...state,
            "arr" : state.arr.filter(item=>item.id != id)
        }
    }
    return state;
}

 

App.js

import React from "react";
import {connect} from "react-redux";
class App extends React.Component {
    constructor() {
        super();
    }
    //增加學生
    add(){
        var name = this.refs.name.value;
        var age = this.refs.age.value;
        this.props.addStudent(name, age)
    }
    render(){
        return <div>
            <p><input type="text" ref="name"/></p>
            <p><input type="text" ref="age"/></p>
            <button onClick={()=>{this.add()}}>增加學生</button>
            {
                this.props.arr.map((item,index)=>{
                    return <p key={item.id}>
                        {item.id} {item.name} {item.age}<button onClick={()=>{this.props.delStudent(item.id)}}>刪除</button>
                    </p>
                })
            }
        </div>
    }
}
export default connect(
//(state) => {
    //    return { arr : state.arr }
    //},
//簡化寫法
    ({arr})=>{
        return { arr }
    },
    (dispatch)=>{
        return {
            addStudent(name, age){
                dispatch({"type" : "ADD_STUDENT", name, age})
            },
            delStudent(id){
                dispatch({"type" : "DEL_STUDENT" , id})
            }
        }
    }
)(App);

原理解析

首先connect之所以會成功,是因為Provider元件:

在原應用元件上包裹一層,使原來整個應用成為Provider的子元件

接收Reduxstore作為props,通過context物件傳遞給子孫元件上的connect

 

connect做了些什麼?

它真正連線 Redux React,它包在我們的容器元件的外一層,它接收Provider提供的 store 裡面的state dispatch,傳給一個建構函式,返回一個物件,以屬性形式傳給我們的容器元件。

 

總結:

connect()(App),第一個()中接受兩個引數,分別是:mapStateToPropsmapDispatchToProps

這兩個引數都是函式,第一個引數函式return的物件的鍵名將自動和props進行繫結,第二個引數函式return的物件的鍵名,也將和props進行繫結。

第一個引數return的物件,是從state中獲得值

第二個引數return的物件,是要改變state的值

 

如果有興趣,可以看一下connect函式的API文件:

https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options

不管應用程式有多大,store只有一個,它就像天神一樣“照耀”所有元件,但預設情況下所有元件是不能得到store的資料的,哪個元件要拿資料,就要connect一下,另外App最大元件確實包裹著所有元件,但不代表App元件連線了就代表其他元件也連線了。

 


三、Redux程式設計-TodoList

http://www.todolist.cn/

reducers/index.js中根據專案情況建立reducer

const initObj = {
    "todos": [
       {"id" : 1, "title" : "吃飯",  "done" : false},
       {"id" : 2, "title" : "睡覺",  "done" : false},
       {"id" : 3, "title" : "打豆豆","done" : false}
    ]
}

export default (state = initObj, action) => {
    return state;
}

 

分別建立TodoHd.jsTodoBd.jsTodoFt.js三個元件:

import React from "react";
export default class TodoHd extends React.Component {
    constructor() {
        super();
    }

    render() {
        return <div>
            <h1>我是TodoHd元件</h1>
        </div>
    }
}
示例程式碼

 

 

 

 

App.js引入元件

import React from "react";
import {connect} from "react-redux";

import TodoHd from "./components/TodoHd.js";
import TodoBd from "./components/TodoBd.js";
import TodoFt from "./components/TodoFt.js";

class App extends React.Component {
    constructor() {
        super();
    }
    
    render() {
        return <div>
            <TodoHd></TodoHd>
            <TodoBd></TodoBd>
            <TodoFt></TodoFt>
        </div>
    }
}
export default connect()(App)  
import React from 'react';
import {connect} from "react-redux";
import TodoItem from "./TodoItem.js";
class TodoBd extends React.Component {
    constructor(props){
        super(props);
    }
    render() {
        return (
            <div>
            //{JSON.stringify(this.props.todos)}
                {
                    this.props.todos.map(item=>{
                    //return <div key={item.id}>{item.title}</div>
                        return <TodoItem key={item.id} item={item}></TodoItem>
                    })
                }
            </div>
        );
    }
}  
//connect目的是問“天”要資料,要通天。
//“通天”是比喻,就是說要問store要資料
export default connect(
    (state)=>{
        return {
            todos : state.todos
        }
    }
)(TodoBd);

 

TodoItem.js

import React from 'react';
export default class TodoItem extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        return (
            <div className="todoItem">
                <input type="checkbox" checked={this.props.item.done}/>
                <span>{this.props.item.title}</span>
                <button>刪除</button>
            </div>
        );
    }
}

 

TodoHd.js增加待辦事項

import React from 'react';
import {connect} from "react-redux";
class TodoHd extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        return (
            <div>
                <input type="text" ref="titleTxt"/>
                <button onClick={()=>{
                    this.props.addTodo(this.refs.titleTxt.value);
                    this.refs.titleTxt.value = "";
                }}>新增</button>
            </div>
        );
    }
}
//這個元件要通天的目的不是要資料,而是改變資料
export default connect(
    null ,
    (dispatch)=>{
        return {
            addTodo(title){
                dispatch({"type":"ADDTODO", title})
            }
        }
    }
)(TodoHd);

 

reducers/index.js寫可預測狀態  

const initObj = {
    "todos" : [
        ...
    ],
}
export default (state = initObj, action)=>{
    if(action.type == "ADDTODO"){
        return {
            ...state, 
            "todos" : [
                ...state.todos, 
                {
                    "id" : state.todos.reduce((a,b)=>{
                        return b.id > a ? b.id : a;
                    },0) + 1,
                    "title" : action.title, 
                    "done" : false
                }
            ]
        }
    }
    return state;
}

TodoFt.js

import React from "react";
import {connect} from "react-redux";
class TodoFt extends React.Component {
    constructor() {
        super();
    }

    render() {
        return <div>
            當前:共{this.props.todos.length}條資訊--
            已做{this.props.todos.filter(item => item.done).length}條--
            未做{this.props.todos.filter(item => !item.done).length}條
        </div>
    }
}

export default connect(
    (state)=>{
        return {
            todos:state.todos
        }
    }
)(TodoFt)
示例程式碼

因為TodoItem這個元件是不通天的,所以TodoItem是不能自己獨立dispatchstore的。

此時就需要TodoBd幫助,因為TodoBd是通天。

 

這是套路:所有被迴圈語句map出來的元件,一律不通天,資料父親給,改變store的能力父親給。

TodoBd.js元件引入了TodoItem.js元件,因為TodoItem元件是被map出來的,所以資訊要傳給每一個TodoItem,而不是讓TodoItem自己通天拿資料。

TodoBd.js

import React from 'react';
import {connect} from "react-redux";
import TodoItem from "./TodoItem.js";

class TodoBd extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        //根據全域性的show屬性來決定當前todos陣列
        if(this.props.show == "ALL"){
            var todos = this.props.todos;
        }else if(this.props.show == "ONLYDONE"){
            var todos = this.props.todos.filter(item=>item.done);
        }else if(this.props.show == "ONLYUNDONE"){
            var todos = this.props.todos.filter(item=>!item.done);
        }

        return (
            <div>
                {
                    todos.map(item=>{
                        return <TodoItem
                            key={item.id}
                            item={item}
                            delTodo={this.props.delTodo.bind(this)}
                            changeTodo={this.props.changeTodo.bind(this)}
                        ></TodoItem>
                    })
                }
            </div>
        );
    }
}
export default connect(
    (state)=>{
        return {
            todos : state.todos ,
            show : state.show
        }
    },
    (dispatch)=>{
        return {
            delTodo(id){
                dispatch({"type" : "DELTODO", id});
            },
            changeTodo(id , k , v){
                dispatch({"type" : "CHANGETODO", id, k, v});
            }
        }
    }
)(TodoBd);

 

TodoItem.js

import React from 'react';
export default class TodoItem extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            "onEdit" : false
        }
    }
    render() {
     const {id, title, done} = this.props.item;
        return (
            <div>
                <input 
                    type="checkbox" checked={done}
                    onChange={(e)=>{
              this.props.changeTodo(id, "done", e.target.checked)
            }}
                />
                {
                    this.state.onEdit 
                    ?
                    <input 
                        type="text" 
                        defaultValue={title} 
                        onBlur={(e)=>{
                            this.props.changeTodo(id,"title", e.target.value)
                            this.setState({"onEdit" : false})
                        }}
                    />
                    :
                    <span onDoubleClick={()=>{this.setState({"onEdit":true})}}>
            {title}
          </span>
                }

                <button onClick={()=>{this.props.delTodo(id)}}>刪除</button>
            </div>
        );
    }
}

 

index.js

const initObj = {
    "todos" : [
        ...
    ],
    "show" : "ALL"       //ALL、ONLYDONE、ONLYUNDONE
}
export default (state = initObj, action) => {
    if(action.type == "ADDTODO"){
        ...
    }else if(action.type == "DELTODO"){
        return {
            ...state, 
            "todos" : state.todos.filter(item => item.id != action.id)
        }
    }else if(action.type == "CHANGETODO"){
        return {
            ...state, 
            "todos" : state.todos.map(item => {
                //如果遍歷到的item項和傳入的aciton的id項不一樣,此時返回原item
                if(item.id != action.id) return item;
                //否則返回修改之後的item
                return {
                    ...item ,
                    [action.k] : action.v
                }
            })
        }
    }else if(action.type == "CHANGESHOW"){
        return {
            ...state, 
            "show" : action.show
        }
    }
    return state;
}

 

TodoFt.js

import React from 'react';
import {connect} from "react-redux";
import classnames from "classnames";

class TodoFt extends React.Component {
    constructor(props) {
        super(props);
    }

    render() {
        return (
            <div>
                <p>
                    當前共{this.props.todos.length}條資訊
做完{this.props.todos.filter(item=>item.done).length}條
未做{this.props.todos.filter(item=>!item.done).length}條
                </p>
                <p>
                    <button className={classnames({"cur":this.props.show == 'ALL'})}                                 
            onClick={()=>{this.props.changeShow('ALL')}}>檢視全部             </button> <button className={classnames({"cur":this.props.show == 'ONLYDONE'})} onClick={()=>{this.props.changeShow('ONLYDONE')}}>僅看已做 </button> <button className={classnames({"cur":this.props.show == 'ONLYUNDONE'})} onClick={()=>{this.props.changeShow('ONLYUNDONE')}}>僅看未做 </button> </p> </div> ); } } export default connect( (state) => { return { todos : state.todos , show : state.show } }, (dispatch) => { return { changeShow(show){ dispatch({"type" : "CHANGESHOW" , show}) } } } )(TodoFt);

四、logger外掛

redux-logger用來輔助開發。

npm install --save redux-logger

改變main.js:

import React from "react";
import ReactDOM from "react-dom";
import {createStore , applyMiddleware} from "redux";
import {Provider} from "react-redux";
import logger from "redux-logger";

import App from "./App.js";
//引入reducer
import reducer from "./reducers/index.js";

//建立store
const store = createStore(reducer , applyMiddleware(logger));

ReactDOM.render(
    <Provider store={store}>
        <App></App>
    </Provider>
    ,
    document.getElementById("app-container")
);

也可以使用redux-devtools這個外掛。

npm install --save-dev redux-devtools
npm install --save-dev redux-devtools-log-monitor
npm install --save-dev redux-devtools-dock-monitor
npm install --save-dev redux-devtools-chart-monitor

文件:

https://github.com/reduxjs/redux-devtools

https://github.com/reduxjs/redux-devtools/blob/master/docs/Walkthrough.md

 


五、combineReducersbindActionCreators

一個網頁的應用程式可能是多個reducer,合併為一個reducer,比如countertodoreducer

 

Redux提供的combineReducers方法,用於reducer的拆分,只要定義各個子reducer函式,然後用這個方法,將它們合成一個大的reducer

Redux提供的bindActionCreators方法,用於通過dispatchaction包裹起來,這條可以通過bindActionCreators建立的方法,直接呼叫dispatch(action)“隱式呼叫”。


5.1 combineReducers

reducers/counter.js就是一個普通的純函式:

export default (state = {"v" : 10},action)=>{
    return state;
}

reducers/todo.js提供的資料:

const initObj = {
    "todos": [
        { "id": 1, "title": "吃飯", "done": false },
        { "id": 2, "title": "睡覺", "done": false },
        { "id": 3, "title": "打豆豆", "done": true }
    ] 
};

export default (state = initObj, action) => {
    return state
}
示例程式碼

 

 

 

 

reducers/index.js要用redux提供的combineReducers來進行智慧合併

import { combineReducers } from "redux";
import counter from "./counter.js";
import todos from "./todos.js";

//暴露合併的reducer
export default combineReducers({
    counter,
    todos
})

 

main.js

import React from "react";
import ReactDOM from "react-dom";
import {createStore} from "redux";
import {Provider} from "react-redux";

import App from "./containers/App.js";
//引入reducer
import reducer from "./reducers";

// 建立 Redux store 來存放應用的狀態。
const store = createStore(reducer);

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

 

containers/App.js元件使用資料

import React from 'react';
import {connect} from "react-redux";
class App extends React.Component {
    constructor(){
        super();
    }
    render() {
        return (
            <div>
                <h1>{this.props.v}</h1>
            </div>
        );
    }
}
export default connect(
    ({counter}) => ({
        v : counter.v
    })
)(App);

 

components/TodoList/index.js元件

import React from "react";
import { connect } from "react-redux";
class TodoList extends React.Component {
    constructor(){
        super();
    }
    render() {
        return (
            <div>
                <h1>我是TodoList</h1>
                {
                    this.props.todos.map(item=>{
                        return <p key={item.id}>{item.title}</p>
                    })
                }
            </div>
        )
    }
};
export default connect(
   ({todos: {todos}})=>{
        return {
            todos
        }
   }
)(TodoList)

 

containers/App.js引入元件:

import React from 'react';
import TodoList from "../components/todolist/index.js";
import Counter from "../components/counter/index.js";
 
export default class App extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        return (
            <div>
                <Counter></Counter>
                <TodoList></TodoList>
            </div>
        );
    }
}

5.2 bindActionCreators

bindActionCreators會將actiondispatch繫結並返回一個物件,這個物件會作為props的一部分傳入元件中。

bindActionCreators主要作用:一般情況下,可以通過Providerstore通過Reactconnext屬性向下傳遞,bindActionCreators的唯一用處就是需要傳遞action creater到子元件,並且該子元件並沒有接收到父元件上傳遞的storedispatch

官方的資料夾結構:https://github.com/reduxjs/redux/tree/master/examples/todomvc/src

 

actions/counterActions.js新建actions資料夾存放type

// 我們把return一個action的函式叫做“action creator”
// 所以這個檔案向外暴露了幾個動作
export const add = () => ({ type: "ADD" })
export const minus = () => ({ type: "MINUS" })
export const cheng = () => ({ type: "CHENG" })
export const chu = () => ({ type: "CHU" })

 

counter/index.js計數器元件

import React from 'react';
import {bindActionCreators} from "redux";
import {connect} from "react-redux";
import * as counterActions from "../../actions/counterActions.js";

class Counter extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        return (
            <div>
                <h1>Counter : {this.props.v}</h1>
                <button onClick={()=>{this.props.counterActions.add()}}>加</button>
                <button onClick={()=>{this.props.counterActions.minus()}}>減</button>
                <button onClick={()=>{this.props.counterActions.cheng()}}>乘</button>
                <button onClick={()=>{this.props.counterActions.chu()}}>除</button>
            </div>
        );
    }
}
export default connect(
    ({counter}) => ({
        v : counter.v
    }),
    (dispatch) => ({
    //這裡的dispatch,等同於store中的store.dispatch,用於組合action
        counterActions : bindActionCreators(counterActions , dispatch)
    })
)(Counter);

 

app/reducers/counter.js

import {ADD, MINUS, CHENG, CHU} from "../constants/COUNTER.js";

export default (state = {"v" : 0} , action) => {
    if(action.type == "ADD"){
        return {
            ...state , 
            "v" : state.v + 1
        }
    }else if(action.type == "MINUS"){
        return {
            ...state , 
            "v" : state.v - 1
        }
    }else if(action.type == "CHENG"){
        return {
            ...state , 
            "v" : state.v * 2
        }
    }else if(action.type == "CHU"){
        return {
            ...state , 
            "v" : state.v / 2
        }
    }
    return state;
}
示例程式碼

 

 

 

 

todolist/index.js

import React from 'react';
import TodoHd from "./TodoHd.js";
import TodoBd from "./TodoBd.js";

export default class TodoList extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        return (
              <div>
                  <h1>TodoList</h1>
                  <TodoHd></TodoHd>
                  <TodoBd></TodoBd>
              </div>
        );
    }
}

 

TodoHd.js

import React from 'react';
import {bindActionCreators} from "redux";
import {connect} from "react-redux";
import * as todoActions from "../../actions/todoActions.js";

class TodoHd extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        return (
            <div>
                <input type="text" ref="titleTxt"/>
                <button onClick={()=>{this.props.todoActions.add(this.refs.titleTxt.value)}}
            >新增
          </button>
            </div>
        );
    }
}
export default connect(
    null ,
    (dispatch) => ({
        todoActions : bindActionCreators(todoActions , dispatch)
    })
)(TodoHd);

 

TodoBd.js

import React from 'react';
import {bindActionCreators} from "redux";
import {connect} from "react-redux";
import * as todoActions from "../../actions/todoActions.js";

class TodoBd extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        return (
            <div>
                {
                    this.props.todos.map(item=>{
                        return <p key={item.id}>
                            {item.title}
                            <button onClick={()=>{this.props.todoActions.del(item.id)}}>
                 刪除
                  </button>
                        </p>
                    })
                }
            </div>
        );
    }
}

export default connect(
    ({todo}) => ({
        todos : todo.todos
    }) ,
    (dispatch) => ({
        todoActions : bindActionCreators(todoActions , dispatch)
    })
)(TodoBd);

 

為了防止actiontype命名衝突,此時要單獨存放在const資料夾中:

app\constants\COUNTER.js

export const ADD = "ADD_COUNTER";
export const MINUS = "MINUS_COUNTER";
export const CHENG = "CHENG_COUNTER";
export const CHU = "CHU_COUNTER";

app\constants\TODO.js

export const ADD = "ADD_TODO";
export const DEL = "DEL_TODO";

然後就可以在以下檔案中,引入以上常量,然後使用大寫的常量替換type字串

l actions中的counterActions.jstodoActions.js

l reducers中的todo.jscounter.js

 

actions/TodoActions.js

import {ADD , DEL} from "../constants/TODO.js";
export const add = (title) => ({"type" : ADD, title});
export const del = (id) => ({"type" : DEL, id});

actions/counterActions.js

import {ADD , MINUS , CHENG , CHU} from "../constants/COUNTER.js";
export const add = () => ({"type" : ADD});
export const minus = () => ({"type" : MINUS});
export const cheng = () => ({"type" : CHENG});
export const chu = (n) => ({"type" : CHU , n});

 

reducers/todo.js

import {ADD , DEL} from "../constants/TODO.js";
const initObj = {
    "todos" : [
        {"id" : 1 , "title" : "吃飯" , "done" : false},
        {"id" : 2 , "title" : "睡覺" , "done" : false},
        {"id" : 3 , "title" : "打豆豆" , "done" : false}
    ]
}
export default (state = initObj , action) => {
    if(action.type == ADD){
        return {
            ...state ,
            "todos" : [
                ...state.todos ,
                {
                    "id" : state.todos.reduce((a,b)=>{return b.id > a ? b.id : a},0) + 1,
                    "title": action.title,
                    "done" : action.done
                }
            ]
        }
    }else if(action.type == DEL){
        return {
            ...state ,
            "todos" : state.todos.filter(item => item.id != action.id)
        }
    }
    return state;
}

 

reducers/counter.js

import {ADD , MINUS , CHENG , CHU} from "../constants/COUNTER.js";

export default (state = {"v" : 0} , action) => {
    if(action.type == ADD){
        return {
            ...state , 
            "v" : state.v + 1
        }
    }else if(action.type == MINUS){
        return {
            ...state , 
            "v" : state.v - 1
        }
    }else if(action.type == CHENG){
        return {
            ...state , 
            "v" : state.v * 2
        }
    }else if(action.type == CHU){
        return {
            ...state , 
            "v" : state.v / action.n
        }
    }
    return state;
}

 

相關文章