React – 07 Redux (1)

前接REACT – 06 範例1 STATE 二訪,透過React本身的State機制,可以很方便改變顯示的原件,不過是有點笨重的感覺,例如deleteTotoItem()

    class App extends React.Component {

        deleteTotoItem(item) {
            ...
            this.setState({todos:newTodos});
        }

        render() {
            ...
            <TodoList todos={this.state.todos} deleteTotoItem={this.deleteTotoItem}/>
        }
    }

    class TodoList extends React.Component {
        ...

        listItem(item) {
            return(
                <TodoListItem ... deleteTotoItem={this.props.deleteTotoItem}/>
            );
        }
        ...
    }

    class TodoListItem extends React.Component {
       ...

        onDeleteClick(event) {
            ...
           this.props.deleteTotoItem(item);
        }
        ...
    }

<App>裡定義的deleteTotoItem(),被傳了兩層才真的在TodoListItem中用到,State也是經過多層的傳遞才拿到,如果覺得這樣是個問題,可以準備使用redux了,如果覺得這不是問題,那接下來這兩篇就是用不到的部份。

簡介

Redux主要由三個部份組成。

  1. Store:用來將所有的State集中存放
  2. Reducer:用來進行State內容的初始化,並真正改變State的資料
  3. Action:這是通知Store目前要做的改變,Store會依Action裡的keyword,找到合適的Reducer來處理

其實我覺得Action比較像是trigger,Reducer這詞單看字面也不太能體會用法。
不過還好,都很簡單...

Action

Action是一個單純的JavaScript Object,裡面只有兩個property,一個是用來充當keyword的type,另一個則是可以隨意命名,用來改變State內容的資料,大概就像下列這樣。

var sample_action = {
    type:'DO_SOMETHING',
    data:''
}

type的值在同一元件內不可重覆,而且為了方便管理,所以最好用const定義出來,像下列這樣

const DO_SOMETHING = 'DO_SOMETHING';
var sample_action = {
    type:DO_SOMETHING,
    date:''
}

Reducer

Reducer是個function,只收兩個變數(currentState, action),然後根據action.type來進行state的改變,例如等會要看到的例子

    const defaultTodos = [
        {
            id:'abc',
            name:'Homework'
        },
        {
            id:'def',
            name:'House keeping'
        }
    ];

    function todosReducer(todos = defaultTodos, action) {
        console.log(`invoke todosReducer with action - ${action.type}`);
        switch (action.type) {
            case ADD_TODOITEM:
                return [ ...todos, {id:Date.now(), name:action.name}];
            case DELETE_TODOITEM:
                return todos.filter((it) => {return (it.id != action.id);});
            default:
                return todos;
        }
    }

todos = defaultTodos這樣是個方便的做法,當一開始State裡的todos還是null的時候,就用defaultTodos來當預設值。再依action.type來處理內容的更動,若遇到不認識的type,就直接回傳收到的todos,實際上在產生Store時,Redux也會故意用INIT當做type,讓state裡的資料能取得預設值完成初始化。

Store

Store基本上就是Component外的State,利用Redux.combineReducers({reducers})來產生,Property的名稱要跟原來的State相同,但Property的值要放reducer。所以如果原來的state是這樣的話

this.state = {
    todos : [
        {
            id:'abc',
            name:'Homework'
        },
        {
            id:'def',
            name:'House keeping'
        }
    ],
    name : ``
}

就要改成

const store = Redux.createStore({
    todos:todosReducer,
    name:nameReducer
});

記得Reducer通常還要負責做預設值初始化的工作,Redux會故意丟一個@@redux/INIT的Action type到reducer裡,強迫每個reducer至少執行一次以取得State的預設值。

透過store.subscribe()來加入listener,當State有所變化時就會執listerner,以便通知Component。

store.dispatch(action)則是依據傳入的action來觸發reducer完成State內容的更動。

store.getState()則可以取得State現在的內容。

實際修改

由於要使用redux,所以要外加兩個javascript library。

<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.6.0/redux.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/5.0.4/react-redux.js"></script>

再來是把所有的State集合取來,並為其產生對應的Action,Reducer

Action

    //定義Action會用到的keyword
    const ADD_TODOITEM = 'ADD_TODOITME';
    const DELETE_TODOITEM = 'DELETE_TODOITEM';
    const UPDATE_NAME = 'UPDATE_NAME';

    //定義產生Action的function
    function createAddTodoItemAction(name) {
        return {
            type : ADD_TODOITEM,
            name : name
        }
    }

    function createDeleteTodoItemAction(id) {
        return {
            type : DELETE_TODOITEM,
            id : id
        }
    }

    function createUpdateNameAction(name) {
        return {
            type : UPDATE_NAME,
            name : name
        }
    }

三個createXXXAction沒有定義也沒關係,寫出來只是如果會有重覆用到時比較不會混淆,但如果只用一次的話,直接在呼叫dispatch裡寫出Action Object也是不錯的方式。

Reducer

    const defaultTodos = [
        {
            id:'abc',
            name:'Homework'
        },
        {
            id:'def',
            name:'House keeping'
        }
    ];

    //加上console.log可以方便觀察redux做了什麼事
    function todosReducer (todos = defaultTodos, action) {
        console.log(`todosReducer invoked with action[${action.type}]`);
        switch (action.type) {
            case ADD_TODOITEM:
                return [ ...todos, {id:Date.now(), name:action.name}];
            case DELETE_TODOITEM:
                return todos.filter((it) => {return (it.id != action.id);});
            default:
                return todos;
        }
    }

    function nameReducer(name = '', action) {
        console.log(`nameReducer invoked with action[${action.type}]`);
        switch (action.type) {
            case UPDATE_NAME:
                return action.name;
            default:
                return name;
        }
    }

重點在於初始化及最後預設回傳值。

Store

    const reducers = Redux.combineReducers({
        todos:todosReducer,
        name:nameReducer
    });

    const store = Redux.createStore(reducers);

依原先State的樣子產生Object參數,只是Property裡的預設值由原來data或object改為Reducer。

Provider

為了方便原來的Component能拿到Store,所以Redux提供了一個ComponentProvider,只要在原本的外加上去,各Component就都能拿到Store了。

//由於目前是透過include javascript的方式,所以需要為ReactRedux.Provider另外命名。
const Provider = ReactRedux.Provider;
ReactDOM.render(<Provider store={store}><App /></Provider>, document.getElementById('Main'));

<Provider store={store}><App /></Provider>就是重點囉。

其他Component的修改

App

    class App extends React.Component {
        componentWillMount() {
            store.subscribe(()=>{this.setState(store.getState())});
        }

        render() {
            return (
                    <div>
                        <TodoFrom />
                        <TodoList />
                    </div>
            );
        }
    }

這裡利用componentWillMount()這個life cycle method來加入listener,store.subscribe(()=>{this.setState(store.getState())});當Store中的值改變時,就回存到State裡來。畢竟Store是外部的東西,要回歸到React裡來還是要依React的規則,使用this.setState()才可以。

其他

其他Component的修改就很容易了,我們不用再把State的值與改變State的function一層層的傳下去,只要直接使用store.getState()store.dispatch()就好。

不過這有點效率上的問題,再進一步會更好。

原始程式可以參考05_Redux_02.html,與先前程式04_ComponentLayout_03.html比較更容易看出差異。

2017/04/24 :
新增兩張圖,說明如何思考將State轉換為StoreReducerAction

State to Store Reducer

Reducer to Action

2 thoughts to “React – 07 Redux (1)”

發表迴響

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料