--- tags: React --- # React - Redux Saga todo-app **gitlab專案連結** : [[Redux saga todo-app]](http://192.168.1.136/robin98727/Redux-Saga-todo-app) 完整畫面[圖片點此](https://i.imgur.com/wPOyjuk.png) ## 1. 目錄架構 ``` Redux-Saga-todo-app |_____ src |_____actions | |_____index.js |_____components | |_____App.jsx |_____containers | |_____App.jsx |_____lib | |_____api.js |_____reducers | |_____clearapi.js | |_____getapi.js | |_____index.js | |_____postapi.js | |_____putapi.js |_____sagas | |_____index.js ``` ## 2. 程式流程 ![Imgur](https://i.imgur.com/etZsqUy.png) ## 3.功能實作 ### 功能一(新增列表) #### 程式碼(components/App.jsx) 對應function ```javascript= //輸入文字 handleSubmit(e) { e.preventDefault() //避免畫面刷新 const taskname = e.target.querySelector('#taskname').value const done = false this.props.fetchPostDataRequest(taskname,done) //發出寫入api請求 this.setState({value: ''}) //清空文字欄剛輸入的文字 } ``` render() ```javascript= //輸入框 <form onSubmit={this.handleSubmit}> <input type = "text" id = "taskname" value={this.state.value} onChange={this.textChange}/> </form> //顯示列表 <ul> { this.props.names.map((d, i) => { return this.isShow(this.props.finishs[i]) && <li key={i}> <input type="checkbox" onChange={(e) =>this.changeDone(e,d,i)} checked={this.props.finishs[i]}/> <span>{d}</span> </li> })} </ul> ``` #### 程式碼(actions/index.js) API post ```javascript= //發出寫入API的請求 export const FETCH_POST_DATA_REQUEST = "FETCH_POST_DATA_REQUEST" export const fetch_post_data_request = (name,finish) => action(FETCH_POST_DATA_REQUEST, { name,finish }) //成功寫入API export const FETCH_POST_DATA_SUCCESS = "FETCH_POST_DATA_SUCCESS" export const fetch_post_data_success = () => action(FETCH_POST_DATA_SUCCESS) //寫入API失敗 export const FETCH_POST_DATA_FAILURE = "FETCH_POST_DATA_FAILURE" export const fetch_post_data_failure = (error) => action(FETCH_POST_DATA_FAILURE, { error }) ``` API get ```javascript= //發出讀取API的請求 export const FETCH_GET_DATA_REQUEST = "FETCH_GET_DATA_REQUEST" export const fetch_get_data_request = () => action(FETCH_GET_DATA_REQUEST) //讀取API成功 export const FETCH_GET_DATA_SUCCESS = "FETCH_GET_DATA_SUCCESS" export const fetch_get_data_success = (name,finish) => action(FETCH_GET_DATA_SUCCESS, { name,finish }) //讀取API失敗 export const FETCH_GET_DATA_FAILURE = "FETCH_GET_DATA_FAILURE" export const fetch_get_data_failure = (error) => action(FETCH_GET_DATA_FAILURE, { error }) ``` #### 程式碼(containers/App.jsx) ```javascript= import { fetch_post_data_request } from '../actions' //api data存在names、finishs的array中 function mapStateToProps(state) { return { names: state.getapi.names, finishs: state.getapi.finishs, } } function mapDispatchToProps(dispatch) { return { fetchPostDataRequest: (name, finish) => dispatch(fetch_post_data_request(name, finish)) }; } ``` #### 程式碼(reducers/postapi.js) ```javascript= import { FETCH_POST_DATA_REQUEST, FETCH_POST_DATA_FAILURE, FETCH_POST_DATA_SUCCESS } from '../actions/index' const InitialState = { err: '', postisloading: false //資料讀取是否讀取中 } export default (state = InitialState, action) => { switch(action.type) { case FETCH_POST_DATA_REQUEST: return { ...state, postisloading: true } case FETCH_POST_DATA_SUCCESS: return { ...state, postisloading: false } case FETCH_POST_DATA_FAILURE: return { ...state, err: action.err, postisloading: false } default: return state } } ``` #### 程式碼(reducers/getapi.js) ```javascript= import { FETCH_GET_DATA_SUCCESS, FETCH_GET_DATA_FAILURE, FETCH_GET_DATA_REQUEST } from '../actions/index' const InitialState = { names: [], //名稱 finishs: [], //是否打勾 err: '', //錯誤訊息 isloading: false //資料是否讀取中 } export default (state = InitialState, action) => { switch(action.type) { case FETCH_GET_DATA_REQUEST: return { ...state, isloading: true } case FETCH_GET_DATA_SUCCESS: return { ...state, names: action.name, finishs: action.finish, isloading: false } case FETCH_GET_DATA_FAILURE: return { ...state, err: action.err, isloading: false } default: return state } } ``` #### 程式碼(sagas/index.js) 發出post的api ```javascript= function* fetchPostData(action) { //將值寫入api const data = yield call(apiCallToFetchPost, action.name, false) if (data) { //判斷是否有資料 //回傳成功 yield put(actionname.fetch_post_data_success()) } else { //回傳失敗 yield put(actionname.fetch_post_data_failure(data.error)) } } //呼叫POST API,使用者輸入後會傳入name,狀態為未勾選 function apiCallToFetchPost(name) { return ( fetch(api_url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(({ title: name, completed: false //未勾選 })) }) .then(res => { if (res.ok) return res.json() //回傳輸入的資料 else return null }) .catch((error) => { console.error(error) }) ) } ``` 當觸發action時呼叫對應function ```javascript= export default function* root() { yield takeLatest([actionname.FETCH_POST_DATA_SUCCESS, actionname.FETCH_GET_DATA_REQUEST, actionname.FETCH_POST_DATA_REQUEST], fetchGetData) yield takeLatest(actionname.FETCH_POST_DATA_REQUEST, fetchPostData) } ``` ### 功能二(勾選資料) #### 程式碼(components/App.jsx) 對應function ```javascript= //改變所有checkbox的勾選狀態 SelectAll(done) { this.props.fetchPutAllDataRequest(done) } //勾選全選的按鈕 handleSelectAll() { const isAll = this.isSelectAll() this.SelectAll(!isAll) } //計算是否全部的checkbox都被勾選 isSelectAll() { if(!this.props.names.length) return false for(let i = 0; i < this.props.names.length; i++) { if(!this.props.finishs[i]) return false } return true //如果全部的checkbox都被點選,全選鈕自動被勾選 } //每次點選checkbox按鈕 changeDone(e,d,i) { //傳入名稱、編號 const check = this.props.finishs[i] this.props.fetchPutDataRequest(d,i,check) } ``` render ```javascript= //全選的按鈕 <input type="checkbox" onChange={this.handleSelectAll} checked={this.isSelectAll()}/> <span>全選</span> //列表以及勾選狀態 <ul> { this.props.names.map((d, i) => { return this.isShow(this.props.finishs[i]) && <li key={i}> <input type="checkbox" onChange={(e) =>this.changeDone(e,d,i)} checked={this.props.finishs[i]}/> <span>{d}</span> </li> })} </ul> ``` #### 程式碼(actions/index.js) 列表勾選 ```javascript= //發出修改勾選狀態請求 export const FETCH_PUT_DATA_REQUEST = "FETCH_PUT_DATA_REQUEST" export const fetch_put_data_request = (name,idx,check) => action(FETCH_PUT_DATA_REQUEST, {name, idx, check }) //修改勾選狀態成功 export const FETCH_PUT_DATA_SUCCESS = "FETCH_PUT_DATA_SUCCESS" export const fetch_put_data_success = () => action(FETCH_PUT_DATA_SUCCESS) //修改勾選狀態失敗 export const FETCH_PUT_DATA_FAILURE = "FETCH_PUT_DATA_FAILURE" export const fetch_put_data_failure = (error) => action(FETCH_PUT_DATA_FAILURE, { error }) ``` 全選鈕 ```javascript= //發出勾選全選鈕請求 export const FETCH_PUT_ALL_DATA_REQUEST = "FETCH_PUT_ALL_DATA_REQUEST" export const fetch_put_all_data_request = (done) => action(FETCH_PUT_ALL_DATA_REQUEST, { done }) //勾選全選鈕成功 export const FETCH_PUT_ALL_DATA_SUCCESS = "FETCH_PUT_ALL_DATA_SUCCESS" export const fetch_put_all_data_success = () => action(FETCH_PUT_ALL_DATA_SUCCESS) //勾選全選鈕失敗 export const FETCH_PUT_ALL_DATA_FAILURE = "FETCH_PUT_ALL_DATA_FAILURE" export const fetch_put_all_data_failure = (error) => action(FETCH_PUT_ALL_DATA_FAILURE, { error }) ``` #### 程式碼(reducers/getapi.js) ```javascript= import { //列表勾選 FETCH_PUT_DATA_SUCCESS, FETCH_PUT_DATA_FAILURE, FETCH_PUT_DATA_REQUEST, //全選 FETCH_PUT_ALL_DATA_SUCCESS, FETCH_PUT_ALL_DATA_REQUEST, FETCH_PUT_ALL_DATA_FAILURE } from '../actions/index' const InitialState = { err: '', check_isdoing: false, check_all_isdoing: false } export default (state = InitialState, action) => { switch(action.type) { //發出勾選列表按鈕請求 case FETCH_PUT_DATA_REQUEST: return { ...state, check_isdoing: true } //勾選列表按鈕成功 case FETCH_PUT_DATA_SUCCESS: return { ...state, check_isdoing: false } //勾選列表按鈕失敗 case FETCH_PUT_DATA_FAILURE: return { ...state, err: action.err, check_isdoing: false } //發出勾選全選按鈕請求 case FETCH_PUT_ALL_DATA_REQUEST: return { ...state, check_all_isdoing: true } //勾選全選按鈕成功 case FETCH_PUT_ALL_DATA_SUCCESS: return { ...state, check_all_isdoing: false } //勾選全選按鈕失敗 case FETCH_PUT_ALL_DATA_FAILURE: return { ...state, err: action.err, check_all_isdoing: false } default: return state } } ``` #### 程式碼(sagas/index.js) 列表勾選 ```javascript= function* fetchPutData(action) { var idx = action.idx + 1 //編號 const name = action.name //名稱 const check = !action.check //勾選 const form = {name,check,idx} const data = yield call(apiCallToFetchPut,form) if (data) { //判斷是否有回傳資料 yield put(actionname.fetch_put_data_success()) //勾選修改成功 } else { yield put(actionname.fetch_put_data_failure(data.error)) //勾選修改失敗 } } function apiCallToFetchPut(form) { const {name, check, idx} = form const data = { title: name ,completed: check} return ( fetch(`http://192.168.1.130:3001/items/${idx}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) .then(res => { if (res.ok) return res.json() else return null }) .catch((error) => { console.error(error) }) ) } ``` 全選 ```javascript= function getItem(i) { return ( fetch(`http://192.168.1.130:3001/items/${i}`, { method: 'GET', headers: { 'Content-Type': 'application/json' } }) .then(res => { if (res.ok) return res.json() else return null }) .catch((error) => { console.error(error) }) ) } function* fetchPutAllData(action) { const alldata = yield call(apiCallToFetchGet) const alldatalen = alldata.length //計算目前API中資料有多少 if(alldata) { for(let i = 1; i <= alldatalen; i++){ //將全部資料修改勾選狀態 const data = yield call(getItem,i) const name = data.title const check = action.done const idx = i const form = {name,check,idx} yield call(apiCallToFetchPut,form) } yield put(actionname.fetch_put_all_data_success()) //修改成功 } else{ yield put(actionname.fetch_put_all_data_failure(alldata.error)) //修改失敗 } } ``` ### 功能三(資料狀態分類) 只修改state上的filter而已 #### 程式碼(components/App.jsx) ```javascript= //計算Active有幾個 count() { let count = 0; this.props.finishs.forEach(item=>{ if(item!=true) count++ }) return count; } //改變分類 changeFilter(filter) { this.setState({ filter: filter }) } //顯示目前所在分類 renderItem(name) { const {filter,changeFilter} = this.state return name===filter? <span>{name}</span> : <a href={'#'+name} onClick={e=>this.changeFilter(name)}>{name}</a> } //顯示分類內的清單列表 isShow(done) { const { filter } = this.state switch(filter) { case 'Active': return done === false case 'Completed': return done === true default: return true } } ``` ### 功能四(等待資料顯示) 每次送出request時,畫面上會顯示資料讀取中 ![讀取中](https://i.imgur.com/vaY53fI.png) 如果request失敗,畫面則顯示資料讀取失敗 ![讀取失敗](https://i.imgur.com/k29oDoP.png)