一、Redux整體感知
Redux是JavaScript狀態管理容器,提供了可被預測狀態的狀態管理容器。來自於Flux思想,Facebook基於Flux思想,在2015年推出Redux庫。
官方git:https://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就是reducer,reducer就是store。
const store = Redux.createStore(reducer);
reducer是一個純函式,而store提供了三個方法:
store.subscribe() 註冊到檢視
store.getState() 得到資料
store.dispatch() 傳送action
學習redux和react結合,到時候就不用註冊到檢視。
然後建立監聽:
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"}) }
點選按鈕,store要dispatch出一個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>
二、Redux和React進行結合開發
在React開發的時候使用Redux可預測狀態容器,要裝兩個新的依賴:
redux:提供createStore、combineReducers、bindActionCreators等功能
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這個函式的第一個引數就是Redux的store,會自動將store的資料作為props繫結到元件上。
connect()的第二個引數:mapDispatchToProps它的功能是,將action作為props繫結到元件上
通俗理解,使用connect可以把state和dispatch繫結到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的子元件
接收Redux的store作為props,通過context物件傳遞給子孫元件上的connect
connect做了些什麼?
它真正連線 Redux 和 React,它包在我們的容器元件的外一層,它接收Provider提供的 store 裡面的state 和 dispatch,傳給一個建構函式,返回一個物件,以屬性形式傳給我們的容器元件。
總結:
connect()(App),第一個()中接受兩個引數,分別是:mapStateToProps、mapDispatchToProps。
這兩個引數都是函式,第一個引數函式return的物件的鍵名將自動和props進行繫結,第二個引數函式return的物件的鍵名,也將和props進行繫結。
第一個引數return的物件,是從state中獲得值
第二個引數return的物件,是要改變state的值
如果有興趣,可以看一下connect函式的API文件:
不管應用程式有多大,store只有一個,它就像天神一樣“照耀”所有元件,但預設情況下所有元件是不能得到store的資料的,哪個元件要拿資料,就要connect一下,另外App最大元件確實包裹著所有元件,但不代表App元件連線了就代表其他元件也連線了。
三、Redux程式設計-TodoList
在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.js、TodoBd.js、TodoFt.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是不能自己獨立dispatch到store的。
此時就需要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
五、combineReducers和bindActionCreators
一個網頁的應用程式可能是多個reducer,合併為一個reducer,比如counter和todo的reducer。
Redux提供的combineReducers方法,用於reducer的拆分,只要定義各個子reducer函式,然後用這個方法,將它們合成一個大的reducer。
Redux提供的bindActionCreators方法,用於通過dispatch將action包裹起來,這條可以通過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會將action和dispatch繫結並返回一個物件,這個物件會作為props的一部分傳入元件中。
bindActionCreators主要作用:一般情況下,可以通過Provider將store通過React的connext屬性向下傳遞,bindActionCreators的唯一用處就是需要傳遞action creater到子元件,並且該子元件並沒有接收到父元件上傳遞的store和dispatch。
官方的資料夾結構: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);
為了防止action的type命名衝突,此時要單獨存放在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.js和todoActions.js
l reducers中的todo.js和counter.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; }