ReactJS - Redux 資料儲存與事件處理 - 筆記長也

ReactJS - Redux 資料儲存與事件處理

2021-11-11 20:14:00   ReactJS

Redux 要做什麼

在基礎入門的文章當中,提及了幾種跨元件傳遞 state 的方式(ReactJS入門 - 各階層Component 溝通),但隨著專案增胖及元件的增加,這些方式都會使得 state 難以維護(每一個元件都必須維護自己的 state,更別說跨元件互動了)。

Redux 可以讓網站所有的 state 統一管理,也能跨元件取用,還能讓資料以及畫面更新分開處理。

安裝

npm install --save redux react-redux

分別安裝 redux 以及 react-redux兩個套件。

Reducer

Reducer 會負責管理 state 狀態,以及處理不同的 action。

先在 src 下建立 reducer 資料夾,並在資料夾中新增一個 todolist.js 的檔案

建立初始值

const initState = {
    todoList: ['第一件事情', '第二件事情'],
  };

這裡建立一組初始資料,這樣比較能知道這個 reducer 是在儲存什麼內容的。

建立 Reducer

const todoReducer = (state = initState, action) => {
  switch (action.type) {
    default:
      return state;
  }
};
export default todoReducer;

Reducer 第一個參數會先將預設的 state 傳給 reducer 儲存,第二個參數則是傳入要 reducer 對 state 做什麼 action(後面會提到)。

在這裏,如果不做任何的 action,預設就是回傳 state。

Store

store 會負責管控 reducer,集中管理專案中的所有 reducer。

先在 src 下建立 store 目錄並在 store 資料夾建立 index.js。

單個 Reducer

如果跟本範例一樣只有一個 reducer ,只需要 import reducer 並呼叫 createStore 就好

import { createStore } from 'redux';
import todoReducer from '../reducer/todolist';

const store = createStore(todoReducer);

export default store;

多個 Reducer

如果日後專案有多個 reducer 需要整合,則需要先呼叫 combineReducers 後,再呼叫 createStore。

import { createStore, combineReducers } from 'redux';
import todoReducer from '../reducer/todolist';
import otherReducer from '../reducer/other';

const allReducer = combineReducers({
  todoReducer,
  otherReducer,
});

const store = createStore(allReducer);

Provider

Provider 會接收指定的 store,與之前 ContextAPI 是差不多的用法。通常我會把 Provider 放在專案的根元件上(大概就是呼叫 ReactDOM.render 的地方,通常是在 index.js)。

我們打開 src/index.js,然後引入 store 以及 Provider,並將元件以 <Provider> 元件包起來,記得將 store 這個 props 指定給 provider。

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import reportWebVitals from './reportWebVitals';
import store from './store';
import { Provider } from 'react-redux';

ReactDOM.render(
  <Provider store={store}>
      <App />
  </Provider>,
  document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

 useSelector

useSelector 可以讓我們把 store 當中的資料提取到元件當中。

先打開 src/App.js ,先引入 useSelector 。

import { useSelector } from 'react-redux';

function App() {
  const todolist = useSelector(state => state.todoList);
  return (
      <div className="App">
        {
          todolist.map(e => (
            <h1>{ e }</h1>
          ))
        }
      </div>
  );
}

export default App;

告訴 useSelector 要取用哪一個 state(這邊取用剛剛由 todoReducer 管理的 todoList)。

基本上到這邊可以執行了,可以看到預設建立的兩個項目。

建立 Action

在完成 store 以及 reducer 的建立之後,我們要來建立不同的 Action 來改變 reducer 所保管的 state。

新增Action

先建立一個 action 目錄,並建立一個 todolist.js 的檔案,並在檔案內宣告一個命令名稱的常數:

export const ADD_TODO = 'ADD_TODO';

這裡為使可讀性高,因此會採用全大寫以分辨常數。

再來要建立傳給 Reducer 的物件,告訴 Reducer 動作以及參數:

export const addTodo = todo => (
    {
        type:ADD_TODO,
        payload:{
            todo,
        }
    }
)

由於 todo 會根據使用者輸入而不同,所以要將其寫成閉包丟入 payload 當中。

新增事件

回到 Reducer 的部分,要告訴 reducer 如何處理不同的 Action。

先打開 src/redcuer/todolist.js ,先將剛剛建立好的 action 檔案引入,並在 switch case 當中插入剛剛建立好的 action:

import * as actions from '../action/todolist'

const initState = {
    todoList: ['第一件事情', '第二件事情'],
  };
  
const todoReducer = (state = initState, action) => {
    switch (action.type) {
        case actions.ADD_TODO:
            return {
                ...state,
                todoList:[
                    ...state.todoList,
                    action.payload.todo,
                ]
            }
        default:
            return state;
    }
};
export default todoReducer;

在 case 當中不用再以字串打 "ADD_TODO "了,可以直接使用剛剛在 action/todolist.js 中建立好的。

回傳更新後的值

要注意的是,最後要 return 一個新的 state ,而不能只是更新之前就的 state,例如:

state.todoList.push(action.payload.todo);

如果只是將新的值 push 到原本的 state,這樣並不會觸發畫面更新。

useDispatch

最後要藉由 useDispatch 去觸發剛才已經建立好的 Action,先打開  src/App.js,並引入 useDispatch 以及要呼叫的 Action:

import { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { addTodo } from './action/todolist'

function App() {
  const todolist = useSelector(state => state.todoList);
  const dispatch = useDispatch();
  const [newTodo, setNewTodo] = useState();
  return (
      <div className="App">
        <input value={newTodo} onChange={(e)=>{ setNewTodo(e.target.value); }}></input>
        <button type="button" onClick={ ()=> {dispatch(addTodo(newTodo));} }>新增</button>
        {
          todolist.map(e => (
            <h1>{ e }</h1>
          ))
        }
      </div>
  );
}

export default App;

然後新增一個 input 以及 button,並以 state 來管理 input 的值。而 button 則透過 dispatch() 來呼叫剛才定義好的 Action 物件,並記得將 newTodo 帶入 addTodo 參數,否則 action.payload.todo 會是 undefined。

之後執行,應該可以得到這樣的結果:

結論

本文已經完成了基本的 redux 使用,要特別注意在 switch case 的 return 部分,必須要回傳一個新的 state 而不能只更新其中的值,否則不會呼叫畫面 re-render。

而之後文章可能搭配其他套件來串接網路的 API,這樣比較符合實際的使用情況。

關於作者


長也

資管菸酒生,嘗試成為網頁全端工程師(laravel / React),技能樹成長中,閒暇之餘就寫一些筆記。 喔對了,也愛追一些劇,例如火神跟遺物整理師,推推。最愛的樂團應該是告五人吧(?)