筆記、[FE303] React 的好夥伴:Redux


Posted by s103071049 on 2021-11-15

Redux 不只能用在 react 身上,他是一個架構、概念。可以套用在任何東西上。

Redux 是實作狀態管理機制的套件,也可以在其他框架看到類似東西。

最早是從 facebook 提 REACT 的概念:FLUX 他是一個架構的東西。Redux 跟 FLUX 有點不一樣。很推薦閱讀 Redux 的官方文件 !

在認識 Redux 之前

狀態管理對前端非常重要 ! 可以想成資料與 UI 之間的對應關係。

對於程式狀態、資料、畫面,不同框架有不同想法

  • Jquery:資料與畫面分開,思考點在怎麼把資料放到畫面上去。要拿資料時,因為東西全都放到畫面上了,要在從畫面上將資料拿回來。
  • Vue and Angular:資料與畫面雙向綁定,可以把資料跟畫面的某部分綁在一起,其中一方改變對方都會變。
  • React:只要管資料。react 會把 ui 刻出來,我們只要管資料與狀態。UI = f(state)。react 比較像是單向,改了 state 才會影響畫面,但改動畫面不會影響 state。

增加複雜度,解決問題:flux

flux 是一個架構相比較的概念為 MVC。最早出來,比 redux 更複雜。他是一個單向的資料流。

想解決的問題:傳統 MVC 中 model 與 view 可能會有很多複雜的關係,view 可以同時有很多 model 的資料,按按鈕會同時改動到很多不同 model 的資料。

專案東西太大,就會有狀態的問題。比如 model 東西被改變,其實很難追蹤誰改變他。flux, redux 要解決的是規模很大的時候,狀態怎麼管理的問題。

ACTION (動作) => DISPATCHER (分發器) => STORE (資料) => VIEW (畫面)

今天要改變 STORE 裡面的資料,只能透過 DISPATCHER(分發器)。EX:按按鈕,VIEW 送 ACTION 到 DISPATCHER,DISPATCHER 再改變 STORE,STORE 再將更新的值傳回 VIEW。

對小軟件而言,VIEW 直接改 STORE 就完了。不用如此費工。但對臉書這麼大的軟件,STORE 改變很難追蹤是哪個 VIEW 變了這裡的資料。當今天出問題,只要找誰發 ACTION 就可以知道誰改進行改動。

  • 推薦看 flux fb 影片。會對整個架構更了解一點。
    # Redux 簡介
    非常推薦官網的動畫!

資料流

  1. [UI]:點 deposit $10 的按鈕
  2. [dispatch]:dispatch 一個 action,action 事實上就是 js 的物件,他裡面會記錄你要做的事情的型態(type: 存錢)與內容(payload: $10)。
  3. [store]:指令進到 store 會通到一個叫 reducer 的地方,類似 array 的 reduce,reduce 有現在的狀態、現在的元素,結合之後返回一個新的狀態。我拿我現在的 state 與新進來的 action,結合後回傳新的 state,所以 state 就會改變
  4. 最後再回傳到 UI 上面去,所以 UI 就會再更新。

redux 重要元素:dispatch, action, store, reducer

useReaducer 類似 Redux 的 hook。

改變東西只有一條路:dispatch 一個 action,透過 reducer 回傳新的 state

Redux 基本操作

react 將 state 放到 component 裡面,redux 將狀態放到 store。store 是 js 的物件。react 與 redux 是兩個不同的概念。

創建 redux store

step1:import redux
step2:write reducer。第一個參數是 state、第二個參數是 action。記得做 state 初始化設定。reducer 這個 function 裡面會對 action 進行不同操作。記住,reducer 決定狀態變化。
step3:建立 store,透過 createStore 這個 function,將我建立好的 reducer 做完參數傳入

const { createStore } = require('redux') // node.js 舊版本無法使用 import 所以我們用 require

// 進行初始狀態設定
const initialState = {
  value: 0
}

// reducer 決定狀態變化
function counterReducer(state = initialState, action) {
  // 針對 action 做出不同操作
  return state // 做甚麼都不變
}

let store = createStore(counterReducer)
console.log(store) // 執行 code => 結果是一串包含很多的 obj
console.log(store.getState()) // { value: 0 }
{
  dispatch: [Function: dispatch],
  subscribe: [Function: subscribe],
  getState: [Function: getState], // 呼叫 getState 可顯示現在的 state
  replaceReducer: [Function: replaceReducer],
  '@@observable': [Function: observable]
}

改變 state

dispatch 一個操作,reducer 就會收到,所以 dispatch 時 action 就會到 reducer 這邊

reducer 可以根據傳來的 action 去決定回傳甚麼樣的 state。

const { createStore } = require('redux')
const initialState = {
  value: 0
}

function counterReducer(state = initialState, action) {
  console.log('received action', action)
  return state 
}

let store = createStore(counterReducer)
store.dispatch({
  type: 'plus' // 慣例是使用 type
})
received action { type: '@@redux/INIT1.1.e.4.9.a' } // redux 建立 store 所初始化的 action

received action { type: 'plus' } // 印出 dispatch 的 action
const { createStore } = require('redux')
const initialState = {
  value: 0
}

function counterReducer(state = initialState, action) {
  console.log('received action', action)
  if (action.type === 'plus') {
    return {
      value: state.value + 1 // 回傳新的 state
    }
  }
  return state
}

let store = createStore(counterReducer)
console.log('first state', store.getState())
store.dispatch({
  type: 'plus'
})
console.log('second state', store.getState())

// 印出的結果如下:
received action { type: '@@redux/INITf.c.d.g.y' }
first state { value: 0 }
received action { type: 'plus' }
second state { value: 1 }

以此類推 ...

const { createStore } = require('redux')
const initialState = {
  value: 0
}

function counterReducer(state = initialState, action) {
  console.log('received action', action)
  if (action.type === 'plus') {
    return {
      value: state.value + 1
    }
  } else if (action.type === 'minus') {
    return {
      value: state.value - 1
    }
  }
  return state
}

let store = createStore(counterReducer)
console.log('first state', store.getState())
store.dispatch({
  type: 'plus'
})
store.dispatch({
  type: 'plus'
})
console.log('second state', store.getState())
store.dispatch({
  type: 'minus'
})
console.log('third state', store.getState())

// 印出的結果如下

received action { type: '@@redux/INITz.4.n.3.g.q' }
first state { value: 0 }
received action { type: 'plus' }
received action { type: 'plus' }
second state { value: 2 }
received action { type: 'minus' }
third state { value: 1 }

當 type 變的越來越多,比較適合用 switch case

function counterReducer(state = initialState, action) {
  console.log('received action', action)
  switch(action.type) {
    case 'plus' : {
      return {
        value: state.value + 1
      }
    }
    case 'minus' : {
      return {
        value: state.value - 1
      }
    }
    default : { // 不在預期 state 裡面
      return state
    }
  }
}

訂閱 store 改變,要做的事

subscribe 可以想成是 listener funct

使用 store.subscribe(這邊傳 funct),表示當 store 有改變,就會觸發我傳進去的 function。

執行流程:

  1. dispatch 後,action 會到 reducer
  2. reducer 回傳新的 state
  3. state 改變後觸發 subscribe 內的 funct。
function counterReducer(state = initialState, action) {
  console.log('received action', action)
  switch(action.type) {
    case 'plus' : {
      return {
        value: state.value + 1
      }
    }
    case 'minus' : {
      return {
        value: state.value - 1
      }
    }
    default : {
      return state
    }
  }
}

let store = createStore(counterReducer)
store.subscribe(() => {
  console.log('changed!', store.getState())
})
store.dispatch({type: 'plus'})

Ex todo-list

action 是 js 的物件,用途為表示想做甚麼樣的操作,可以自行定義。

  • type
  • payload (你想傳的參數)
const { createStore } = require('redux')
const initialState = {
  todos: []
}

function counterReducer(state = initialState, action) {
  console.log('received action', action)
  switch(action.type) {
    case 'add_todo' : {
      return { 
        // 同 react 是 immutable
        todos: [...state.todos, {
          name: action.payload.name
        }]
      }
    }
    default : {
      return state
    }
  }
}

let store = createStore(counterReducer)
store.subscribe(() => {
  console.log('changed!', store.getState())
})
store.dispatch({
  type: 'add_todo',
  payload: {
    name: 'i am new todo'
  }
})

// console 的結果
received action { type: '@@redux/INITv.z.b.a.k.d' }
received action { type: 'add_todo', payload: { name: 'i am new todo' } }
changed! { value: [ { name: 'i am new todo' } ] }

實際開發會有更多 state , 所以上述寫法會出問題。因為 return 的東西會取代原本的東西變成一個全新的 state。解決方法,透過解構將之前的狀態保存,只針對要修改的部分去做操作

const initialState = {
  email: '123@gmail.com',
  todos: []
}

//結果 email 不見囉
changed! { value: [ { name: 'i am new todo' } ] } 

// 解決方式
  switch(action.type) {
    case 'add_todo' : {
      return { // 同 react immutable 所以不能直接改
        ...state, // 將之前的 state 保存起來,只改我要改的
        todos: [...state.todos, {
          name: action.payload.name
        }]
      }
    }

幫每個 todo 加上 id

const { createStore } = require('redux')
const initialState = {
  email: '123@gmail.com',
  todos: []
}
let todoId = 0
function counterReducer(state = initialState, action) {
  console.log('received action', action)
  switch(action.type) {
    case 'add_todo' : {
      return { 
        ...state, 
        todos: [...state.todos, {
          id: todoId++,
          name: action.payload.name
        }]
      }
    }
    default : {
      return state
    }
  }
}

let store = createStore(counterReducer)
store.subscribe(() => {
  console.log('changed!', store.getState())
})
store.dispatch({
  type: 'add_todo',
  payload: {
    name: 'todo0'
  }
})
store.dispatch({
  type: 'add_todo',
  payload: {
    name: 'todo1'
  }
})

實作刪除功能

const { createStore } = require('redux')
const initialState = {
  email: '123@gmail.com',
  todos: []
}
let todoId = 0
function counterReducer(state = initialState, action) {
  console.log('received action', action)
  switch(action.type) {
    case 'add_todo' : {
      return {
        ...state,
        todos: [...state.todos, {
          id: todoId++,
          name: action.payload.name
        }]
      }
    }
    case 'delete_todo' : {
      return {
        ...state,
        todos: state.todos
          .filter(todo => todo.id !== action.payload.id)
      }
    }
    default : {
      return state
    }
  }
}

let store = createStore(counterReducer)
store.subscribe(() => {
  console.log('changed!', store.getState())
})
store.dispatch({
  type: 'add_todo',
  payload: {
    name: 'todo0'
  }
})
store.dispatch({
  type: 'add_todo',
  payload: {
    name: 'todo1'
  }
})

// 刪除 id 是 0 的 todo
store.dispatch({
  type: 'delete_todo',
  payload: {
    id: 0
  }
})

reducer 優點:

  1. 好寫測試
  2. reducer is a pure function,不會在裡面 call api、寫 localStorage,只會做的就是回傳一個 state
// 優點一、好寫測試
expect(counterReducer(initialState, {
  type: 'add-todo',
  payload: {
    name: '123'
  }
})).toEqual({
  todos: [{name: '123'}]
})

打錯字的好幫手:action constants

當 todos 越變越多如果打錯字怎麼辦 ? add-todo 打成 addd-todo ? 所以有了 action constants

若打錯字,redux 會報錯告知 ActionTypes 沒有這個屬性,是不是拼錯了

若使用字串方式,打錯字不會有任何的發現,因為你還是傳了一個 type 出來,只是沒有處理,對沒有處理的 type,reducer 不會做任何報錯告知

const { createStore } = require('redux')
// action constants
const ActionTypes = {
  ADD_TODO: 'add_todo',
  DELETE_TODO: 'delete_todo'
}
const initialState = {
  email: '123@gmail.com',
  todos: []
}
let todoId = 0
function counterReducer(state = initialState, action) {
  console.log('received action', action)
  switch(action.type) {
    case  ActionTypes.ADD_TODO: {
      return { // 同 react immutable 所以不能直接改
        ...state, // 將之前的 state 保存起來,只改我要改的
        todos: [...state.todos, {
          id: todoId++,
          name: action.payload.name
        }]
      }
    }
    case  ActionTypes.DELETE_TODO : {
      return {
        ...state,
        todos: state.todos
          .filter(todo => todo.id !== action.payload.id)
      }
    }
    default : {
      return state
    }
  }
}

let store = createStore(counterReducer)
store.subscribe(() => {
  console.log('changed!', store.getState())
})
store.dispatch({
  type: ActionTypes.ADD_TODO,
  payload: {
    name: 'todo0'
  }
})
store.dispatch({
  type: ActionTypes.ADD_TODO,
  payload: {
    name: 'todo1'
  }
})
store.dispatch({
  type: ActionTypes.DELETE_TODO,
  payload: {
    id: 0
  }
})

action creator

解決問題 :每次都要打 type 與 payload 超麻煩
所以可以將重複的部份包成 function,透過呼叫 function,回傳 action

function addTodo(name) {
  return {
    type: ActionTypes.ADD_TODO,
    payload: {
      name
    }
  }
}
function deleteTodo(id) {
  return {
    type: ActionTypes.DELETE_TODO,
    payload: {
      id
    }
  }
}
store.dispatch(addTodo('todo0'))
store.dispatch(addTodo('todo1'))
store.dispatch(deleteTodo(0))

action creator 與 action types 都只是幫忙我們開發上比較少錯誤!

  • Redux 有提供 library 可以傳多個 reducer。比方說 todo 是一個 reducer、user 又是一個 reudcer,藉由不同 reducer 處理不同 action。

React-redux:套上 React

recall redux 基本概念:

reducer

  • 建立 reducer 這個 function,透過接收 state 與 action 回傳新的 state ( ie newState = reducer(currentState, action) )

dispatch

  • dispatch action 會進來 reducer 這邊,透過接收現在的 state 與傳進來的 action 回傳新的 state
  • dispatch type 與 payload 會有 typeError => 解決方法:宣告 const ActionTypes;typeAnnoying => 解決方法:包成 function,透過 action creator 只要 call function 他就會回傳給我相對應的 js 物件

store

  • store.getState(),將狀態取出

react-redux 介紹

目前官方推薦使用 redux-toolkit。但工作碰到的 library 有可能沒那麼新,所以還是有必要知道他是甚麼。底層是相同,但提供的 functino 不太一樣。

step1:src 建立資料夾 redux,將 redux 相關的都放在裡面

step2:redux 資料夾中建立 store.js。import reducer 透過 createStore() 將 reducer 傳入

import { createStore } from "redux";
import rootReducer from "./reducers";
export default createStore(rootReducer);

step3:建立 reducers 的資料夾,裡面建立各別的 reducer,如:todos.js, users.js;同一層建立 index.js 目的是結合 reducer 們,透過地工具為combineReducers()

// export default reducer funct

import { ADD_TODO, DELETE_TODO } from "../actionTypes";

const initialState = {
  todos: [],
};
let todoId = 0;
export default function todoReducer(state = initialState, action) {
  console.log("received action", action);
  switch (action.type) {
    case ADD_TODO: {
      return {
        // 同 react immutable 所以不能直接改
        ...state, // 將之前的 state 保存起來,只改我要改的
        todos: [
          ...state.todos,
          {
            id: todoId++,
            name: action.payload.name,
          },
        ],
      };
    }
    case DELETE_TODO: {
      return {
        ...state,
        todos: state.todos.filter((todo) => todo.id !== action.payload.id),
      };
    }
    default: {
      return state;
    }
  }
}
import { ADD_USER } from "../actionTypes";
const initialState = {
  users: [],
};
export default function usersReducer(state = initialState, action) {
  switch (action.type) {
    case ADD_USER: {
      return {
        ...state,
        users: [
          ...state.users,
          {
            name: action.payload.name,
          },
        ],
      };
    }
    default: {
      return state;
    }
  }
}

可以把 reducer 想成種類。要留意,store 產生的東西和你想的不太一樣。

import { combineReducers } from "redux";
import todos from "./todos";
import users from "./users";
export default combineReducers({
  todos, // 表示 todos 這個狀態我要用 todosReducer 負責
  users,
});

// store 的資料結構
{
    todos: {
        // todosReducer
        todos: []
    }
    users: {
        // usersReducer
        users: []
    }
}

也可以這樣命名

export default combineReducers({
  todoState: todos,
  users,
});

step4:在 redux 裡面建立 actionType.js。官方推薦的寫法不是物件。

// 原本的寫法
const ActionTypes = {
    ADD_TODO: "add_todo",
    DELETE_TODO: "delete_todo",
    ADD_USER: "add_user"
  }

// 官方寫法
export const ADD_TODO = "add_todo";
export const DELETE_TODO = "delete_todo";
export const ADD_USER = "add_user";

step5:在 redux 裡面建立 actions.js,就是前面介紹的 action creator

import { ADD_TODO, DELETE_TODO, ADD_USER } from "./actionTypes";
export function addTodo(name) {
  return {
    type: ADD_TODO,
    payload: {
      name,
    },
  };
}
export function deleteTodo(id) {
  return {
    type: DELETE_TODO,
    payload: {
      id,
    },
  };
}
export function addUser(name) {
  return {
    type: ADD_USER,
    payload: {
      name,
    },
  };
}

my-app/src/index.js

redux 背後是用 context 但他做了更多額外的事情,因為 context 某些時候效能沒那麼好。這樣一來 App 就可以拿到東西

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

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

my-app/src/App

通常對 store 裡面的資料需要做一些轉換,redux 提供 useSelector 讓我們可以選到 state。他的本質是 function,帶的參數為 store 的 state

import React from "react";
import { useSelector } from "react-redux";
function App() {
  const state = useSelector((state) => state);
  console.log("state", state);
  return <div>123</div>;
}

export default App;

如果在每個需要的地方各別拿出會變得太長。

function App() {
  const todos = useSelector((store) => store.todoState.todos);
  console.log("state", todos);
  return <div>123</div>;
}

可以在 store.js 同層開一個 selectors.js

import { selectTodos } from "./redux/selectors";
function App() {
  const todos = useSelector(selectTodos);
  console.log("state", todos);
  return <div>123</div>;
}
export const selectTodos = (store) => store.todoState.todos;

透過 useDispatch( ) 拿 dispatch

import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { selectTodos } from "./redux/selectors";
import { addTodo } from "./redux/actions";
function App() {
  const todos = useSelector(selectTodos);
  const dispatch = useDispatch();
  console.log("state", todos);
  return (
    <div>
      <button
        onClick={(e) => {
          dispatch(addTodo(Math.random()));
        }}
      ></button>
      <ul>
        {todos.map((todo) => (
          <li>
            {todo.id} {todo.name}
          </li>
        ))}
      </ul>
    </div>
  );
}

redux devtool 介紹

要在 DEV 上做一些設定才能存取到。git-hub 連結

在沒有 hooks 以前:connect

connect 可以將 react 的 component 跟 redux 結合在一起。

connect 是一個 funct,他會傳兩個參數:

  1. mapStateToProps (type-funct, 會把 state 給我,可以 return 想要拿的 state,就會變成我 component 的 props)、概念類似 selector
  2. mapDispatchToProps 會給我 dispatch,若 props 名稱 與 action 名稱一樣,可以直接簡化成傳物件
const mapDispatchToProps = {
  addTodo,
};

等價

const connectToStore = connect(mapStateToProps, mapDispatchToProps);
const ConnectedAddTodo = connectToStore(AddTodo);
export default ConnectedAddTodo; // smart component, container

export default connect(mapStateToProps, mapDispatchToProps)(AddTodo)

透過包了很多 function 跟 redux 串接起來。這種方式就是 HOC (component 再包一層 component)

這種做法與 hooks 的作法,最大差異在於 AddTodo 不知道 redux 的存在。

import React from "react";
import { connect } from "react-redux";
import { addTodo } from "./redux/actions";

// dumb component 不知道有 redux 的存在,僅負責 ui 的顯示
function AddTodo({ addTodo }) {
  return (
    <button
      onClick={() => {
        addTodo(Math.random());
      }}
    >
      add todo
    </button>
  );
}
const mapStateToProps = (store) => {
  return {
    todos: store.todos.todos,
  };
};
const mapDispatchToProps = (dispatch) => {
  return {
    addTodo: (payload) => dispatch(addTodo(payload)),
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(AddTodo);

通常會再開資料夾 containers 與 components
這種寫法方便測試,components 的測試歸 components;redux 的測試歸 redux

// components 不存取 redux 的東西
export default function AddTodo({ addTodo }) {
  return (
    <button
      onClick={() => {
        addTodo(Math.random());
      }}
    >
      add todo
    </button>
  );
}
// containers 才會知道 redux 的存在
import { connect } from "react-redux";
import { addTodo } from "../redux/actions";
import AddTodo from "../components/AddTodo";
// dumb component 不知道有 redux 的存在,僅負責 ui 的顯示

const mapStateToProps = (store) => {
  return {
    todos: store.todos.todos,
  };
};
const mapDispatchToProps = (dispatch) => {
  return {
    addTodo: (payload) => dispatch(addTodo(payload)),
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(AddTodo);

也可以用 hooks 的方式實作 containers

實作簡易 todo list

只做新增、刪除功能。redux 更適合場合是 global 狀態,比方說登入紀錄。 component 沒有存取好會有效能上的隱憂。

改變 state 的方式和 context 不一樣。也可以用 context 加上 useReducer 做到相同效果,但 redux 還幫我們處理一些效能上的問題。

// AddTodo components 
import { useState, Fragment } from "react";
export default function AddTodo({ addTodo }) {
  const [value, setValue] = useState("");
  return (
    <Fragment>
      <input value={value} onChange={(e) => setValue(e.target.value)} />
      <button
        onClick={() => {
          addTodo(value);
          setValue("");
        }}
      >
        add todo
      </button>
    </Fragment>
  );
}

不建議 hooks 與 connect 混著寫

// app
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { selectTodos } from "./redux/selectors";
import AddTodo from "./containers/AddTodo";
import { deleteTodo } from "./redux/actions";
function App() {
  const todos = useSelector(selectTodos);
  const dispatch = useDispatch();
  return (
    <div>
      <AddTodo />
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            {todo.id} {todo.name}
            <button onClick={() => dispatch(deleteTodo(todo.id))}>
              delete
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

#Redux #react-redux #useReducer







Related Posts

[ Day 05 ] 用 Puppeteer 來做自動化機器人吧 (四) : Dockerize 篇

[ Day 05 ] 用 Puppeteer 來做自動化機器人吧 (四) : Dockerize 篇

[C#] 寄送含有會議的信件至outlook

[C#] 寄送含有會議的信件至outlook

單元測試 (Unit Test)

單元測試 (Unit Test)


Comments