接下來 3 篇文章的目標是以 React + Redux + Webpack 的技術來完成 To-Do List 功能與 PWA,而內容會假設讀者已經熟悉 React, Redux 及 Webpack,會把重點放在如何實作,不會逐步講解基礎觀念。
本篇為第 2 篇: 建立檔案架構與功能
放置網站的圖片資源
入口檔案
針對檔案架構個別說明如下:

App.js 代表整個 Application,屬於應用程式最外層的元件,可以把固定的版面放這裡,此範例放置 Header 元件
import React, { Component } from 'react';
import Header from '../Component/Header.jsx';
export default class App extends Component {
    render() {
        return (
            <div>
                { /* 應用程式最外層的元件  */ }
                { /* 把固定的版面放這裡  */ }
				<Header />
                { this.props.children }
            </div>
        );
    }
}

Home.js 用來放置主要內容,這裡放 Main 元件
import React, { Component } from 'react';
import { connect } from 'react-redux';
import Main from '../Container/Main.jsx';
class Home extends Component {
    render() {
        return (
            <div>
                <Main />
            </div>
        );
    }
}
export default (Home);

Main.jsx 包括 InputContainer 和 TodoListContainer.jsx
import React, { Component } from 'react';
import Input from './InputContainer.jsx';
import TodoListContainer from './TodoListContainer.jsx';
export default class Main extends Component {
    render() {
        return (
            <div>
                <Input />
                <TodoListContainer />
            </div>
        );
    }
}

InputContainer 的程式碼傳入 Input 元件所需的 action creator 為 addTodoList,用來處理『新增待辦事項』的方法
import { connect } from 'react-redux';
import Input from '../Component/Input.jsx';
import { addTodoList } from '../Reducers/todolist.js';
export default connect(null, {
    addTodoList
})(Input);

TodoListContainer 的程式碼傳入 TodoList 元件所需的 store(state.todolist) 和 action creator,store 包括 id、是否完成狀態、待辦事項文字; action creator 則有三個方法,分別是 getTodoList (取得待辦事項)、toggleTodoList (修改待辦事項)、delTodoList (刪除待辦事項)
import TodoList from '../Component/TodoList.jsx';
import { connect } from 'react-redux';
import { getTodoList, toggleTodoList, delTodoList } from '../Reducers/todolist.js';
export default connect(state => ({
    todos: state.todolist
}), {
    getTodoList,
    toggleTodoList,
    delTodoList
})(TodoList);
放置 LOGO 和顯示未完成的個數

import React, { Component } from 'react';
import { connect } from 'react-redux';
class Header extends Component {
    render () {
        const { unfinishedCount } = this.props;
        return (
            <header className="header">
                <img
                    className="logo"
                    src="./assets/images/logo_todo.png"
                    alt=""
                />
                <div className="unfinished">
                    <span className="count">{ unfinishedCount }</span>
                    <span>個未完成</span>
                </div>
            </header>
        );
    }
}
export default connect(state => {
    return {
        unfinishedCount: state.todolist
                        .filter((item)=>!item.isComplete).length
    }
})(Header);
輸入 To-Do List 內容

import React, { Component } from 'react';
export default class Input extends Component {
    constructor (props) {
        super(props);
        this.state = {
            value: ''
        }
    }
    render() {
        const { addTodoList } = this.props;
        return (
            <div id="todoInput" className="input-content">
                <img
                    className="add"
                    src="./assets/images/ic_add.png"
                    alt=""
                />
                <input
                    className="input"
                    placeholder="What need to be done?"
                    value={ this.state.value }
                    type="text"
                    onChange={
                        (event) => {
                            this.setState({
                                value: event.target.value
                            });
                        }
                    }
                    onKeyDown={
                        (event) => {
                            if (event.keyCode === 13) {
                                console.log('Enter');
                                addTodoList(this.state.value);
                                this.setState({value: ''})
                            }
                        }
                    }
                />
            </div>
        );
    }
}
顯示 To-Do List 內容(ul)

import React, { Component } from 'react';
import TodoListItem from './TodoListItem.jsx';
export default class TodoList extends Component {
    componentDidMount() {
        this.props.getTodoList();
    }
    render() {
        const { todos, toggleTodoList, delTodoList } = this.props;
        return (
            <ul id="todoList">
            {
                todos.map((item, index) => {
                    return (
                        <TodoListItem
                            key={`todolist_${index}`}
                            id={ item.id }
                            desc={ item.desc }
                            isComplete={ item.isComplete }
                            toggleTodoList={ toggleTodoList }
                            delTodoList={ delTodoList }
                        />
                    )
                })
            }
            </ul>
        );
    }
}
顯示 To-Do List 項目(li)

import React, { Component } from 'react';
export default class TodoListItem extends Component {
	constructor(props) {
		super(props);
        this.modifyList = this.modifyList.bind(this);
	}
    modifyList() {
        const { isComplete, id, desc } = this.props;
        this.props.toggleTodoList({
            id,
            desc,
            isComplete: !isComplete
        });
    }
	render() {
		const { isComplete, id, desc, delTodoList } = this.props;
		return (
			<li className="list">
                <a
                	className={ isComplete ? 'finish' : 'unfinished' }
                	onClick={ () =>{this.modifyList();} }
                >
                </a>
                <p
                    className="desc"
                    onClick={ () =>{this.modifyList();} }
                >
                    { desc }
                </p>
                <a
                    className="del"
                    onClick={ () => { delTodoList(id); } }
                >
                </a>
            </li>
		);
	}
}
Reducers 則用來存放資料,搭配 action type、action creator 處理資料,再用 Reducers/index.js 整合 Reducers,此範例只有一個 Reducer (todolist.js)。
先前文章已使用 Vanilla JS 完成 To-Do List 功能,複習一下之前實作 Vanilla JS 的相關文章:
這次則是要改成 React + Redux 實作功能。
todolist 的 Reducer 方法如下,用來處理 todolist 資料:
export default function todolist (state = initialState, action) {
    switch (action.type) {
    case RECEIVE_TODO:
        return action.data;
    case ADD_TODO:
        state.push(action.payload);
        return [...state];
    case TOGGLE_TODO:
        state.forEach((item, index) => {
            if (item.id === action.payload.id) {
                state[index] = action.payload;
            }
        })
        return [...state];
    case DEL_TODO:
        return state.filter(item => item.id !== action.id)
    default:
        return state;
    }
}
action creator 如下:
export function receiveTodoList (data) {
    return {
        type: RECEIVE_TODO,
        data: data
    };
}
export function getTodoList() {
    return function(dispatch) {
        return axios.get(`${URL}todolist`)
        .then(res => {
            dispatch(receiveTodoList(res.data));
        });
    }
}
export function receiveAddTodoList (payload) {
    return {
        type: ADD_TODO,
        payload: payload
    };
}
export function addTodoList (value) {
    return function(dispatch) {
        return axios({
            method: 'post',
            url: `${URL}todolist`,
            headers: {
                'Content-Type': 'application/json'
            },
            data: {
                isComplete: false,
                desc: value
            }
        })
        .then(res => {
            dispatch(receiveAddTodoList(res.data));
        })
        .catch(error => { console.log(error) })
    }
}
export function toggleTodoList (payload) {
    return function(dispatch) {
        return axios({
            method: 'put',
            url: `${URL}todolist/${payload.id}`,
            headers: {
                'Content-Type': 'application/json'
            },
            data: payload
        })
        .then(res => {
            dispatch({type: TOGGLE_TODO, payload: payload});
        })
    }
}
export function delTodoList (id) {
    return function(dispatch) {
        return axios({
            method: 'delete',
            url: `${URL}todolist/${id}`,
            headers: {
                'Content-Type': 'application/json'
            }
        })
        .then(res => {
            dispatch({type: DEL_TODO, id: id});
        })
    }
}
補充說明: 元件為什麼要拆分為
Container和Component?
可以參考 Container Components 和 Presentational and Container Components 這 2 篇文章,當然目前示範的 To-Do List 是小範例,但如果專案越來越龐大,為了重用性、可維護性等原因,建議拆分  Container 和 Component ,由 Container 處理資料,讓 Component 專心顯示內容。
此篇文章針對『建立檔案架構與功能』做說明,從規劃專案的架構開始,新增各種元件去顯示畫面,接著建立 Redux 的 Store 、 Reducer 和 Action creator 完成待辦事項的功能,而下一篇將會介紹『實作 PWA』。
本人小小筆記,如有錯誤或需要改進的部分,歡迎給予回饋。
我將會用最快的速度修正,m(_ _)m。謝謝