---
# System prepended metadata

title: React - Redux Saga todo-app
tags: [React]

---

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