那些 React 跟 Redux 在一起的日子


Posted by Christy on 2021-12-24

本文為 Lidemy [FE303] Redux 學習筆記,老師推薦去看 官方文件 學習 Redux 可以學到更多更完整。

零、概括總結

學前介紹了 redux 基礎概念,知道歷史由來、使用背景,透過基本操作來熟悉 redux 執行流程,實作簡易 todo 並優化程式碼,接著把 redux react 串在一起使用。

串在一起有兩種方式,一種是利用 hooks,另一種則是 connect。

最後實作了我們的老朋友 todo 的新增、刪除功能,本週作業是以 redux 改寫 W21 的 todo list

1. 學前概念

  • Redux 是實作狀態管理機制的套件

  • 跟 React 不太一樣的 Vue & Angular,是資料與畫面雙向綁定

  • Flux 比 Redux 再複雜一點,Redux 經過演化,簡化了一些概念

2. Redux 基本操作

安裝 -> 引入 -> 創建 store -> dispatch 任務

3. 小小優化:避免錯誤 action constants、簡化流程 action creator

4. 當我們串在一起:react + redux

安裝 react-redux & redux → 設置 redux 資料夾及檔案 → 設置 index.js & App.js → 跑一些內容試試看

一、Redux 簡介

1. Redux 簡介

官網動畫介紹

這個流程跟 React hook 的 useReducer 很像

心得:像是叫 uber eats,dispatcher 有種總機的感覺,可以一次叫蔥油餅、珍奶在不同店家,吳柏毅送過來

2. Redux 基本操作

影片在 [FE303] > Redux 簡介 > Redux 基本操作

0'00" - 9'30" 解釋 redux 基本運作過程,印出不同狀態來理解執行順序

action -> dispatch -> reducer(store) -> subscribe

React 跟 Redux 差別在於,前者把狀態放在 component 裡面;後者把狀態放在 store 裡。兩者可分開操作,以下為從頭開始安裝過程:

2.1 只裝 redux:參考資料:Getting Started with Redux

a. 在相對應資料夾底下,$ npm install redux

b. 新增 app.js 檔案,引入 redux const { createStore } = require('redux')

不用 import 因 node.js 舊版沒有支援

c. 先創建 store 並用 node app.js 印出看看有什麼

// 以下是印出內容
{
  dispatch: [Function: dispatch],
  subscribe: [Function: subscribe],
  getState: [Function: getState],
  replaceReducer: [Function: replaceReducer],
  '@@observable': [Function: observable]
}

// 以下為 app.js 內容
const { createStore } = require('redux')

const initialState = {
  value: 0
}

function counterReducer(state = initialState, action) {
  return state;
}

let store = createStore(counterReducer)

console.log(store)

d. 呼叫 getState() 顯示現在 state { value: 0 }

const { createStore } = require('redux')

const initialState = {
  value: 0
}

function counterReducer(state = initialState, action) {
  return state;
}

let store = createStore(counterReducer)

console.log(store.getState())

e. 來派任務了:

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

console.log(store.getState())
// 第一個是 redux 自動的初始化,不用管
received action { type: '@@redux/INITu.r.5.u.u.j' }
// 任務在這裡
received action { type: 'plus' }
// 這是 state
{ value: 0 }

f. subscribe 訂閱開啟小鈴鐺:就像加上監聽器的功能

// 裡面傳一個函式,當 state 改變時,就會印出新的 state

store.subscribe(() => {
  console.log('new state', store.getState())
})

store.dispatch({
  type: 'plus'
})

總結一下:action 經過 dispatch 以後,進入 reducer 裡面回傳新的狀態,接著觸發 subscribe 監聽變化

g. [FE303] > Redux 簡介 > Redux 基本操作 09'30" - 14'50": 實作簡易 todo

payload 是想傳的參數

const  { createStore } = require('redux')

let todoId = 1
const initialState = {
  email: 'aaa',
  todos: []
}

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('new state', store.getState())
})

store.dispatch({
  type: 'add_todo',
  payload: {
    name: 'I am new todo'
  }
})

store.dispatch({
  type: 'add_todo',
  payload: {
    name: 'I am new todo2'
  }
})

store.dispatch({
  type: 'delete_todo',
  payload: {
    id: 1,
  }
})

h. [FE303] > Redux 簡介 > Redux 基本操作 09'52": 寫測試

expect(counterReducer(initialState, {
  type: 'add_todo',
  payload: {
    name: '123'
  }
})).toEqual({
  // 結果是否如所想
  todos: [{ name: '123' }]
})

reducer 是一個 pure function,作用單純,只會修改、刪除等等

i. [FE303] > Redux 簡介 > Redux 基本操作 15'50":利用 action constants 避免錯字

有 type 但沒有處理的 redux 不會報錯,因此規範 ActionTypes 並把所有的 type 都用規定好的字元取代,有錯字 redux 就會報錯

const  { createStore } = require('redux')

const ActionTypes = {
  ADD_TODO: 'add_todo',
  DELETE_TODO: 'delete_todo'
}

let todoId = 1

const initialState = {
  email: 'aaa',
  todos: []
}

function counterReducer(state = initialState, action ) {
  console.log('received action', action)
  switch(action.type) {
    case ActionTypes.ADD_TODO: {
      return {
        ...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('new state', store.getState())
})

store.dispatch({
  type: ActionTypes.ADD_TODO,
  payload: {
    name: 'I am new todo'
  }
})

store.dispatch({
  type: ActionTypes.ADD_TODO,
  payload: {
    name: 'I am new todo2'
  }
})

store.dispatch({
  type: ActionTypes.DELETE_TODO,
  payload: {
    id: 1,
  }
})

j. [FE303] > Redux 簡介 > Redux 基本操作 16'20" action creator,把行為寫成一個函式包起來,取代 store.dispatch()

function addTodo(name) {
  return {
    type: ActionTypes.ADD_TODO,
    payload: {
      name
    }
  }
}

function deleteTodo(id) {
  return {
    type: ActionTypes.DELETE_TODO,
    payload: {
      id
    }
  }
}
store.dispatch(addTodo('todo1'))
store.dispatch(addTodo('todo2'))
store.dispatch(deleteTodo(1))

k. [FE303] > Redux 簡介 > Redux 基本操作 21'09":結合 react redux

如果不用 redux,也可以自行串接,原理利用 store.subscribe() 訂閱狀態變化,當使用者按下按鈕時,就 store.dispatch() 派送任務,接著 store 處理任務並回傳新的狀態產生畫面

這裡說明怎麼手動把 react redux 串接在一起以及背後原理,接下來會學習怎麼自動結合兩者

2.2 react + redux:參考資料:Getting Started with React Redux

a. 從頭開始用 $ npx create-react-app todo --template redux

註:react 不提供 global 安裝了,出現錯誤訊息,參考 Create-react-app 錯誤訊息

b. 現有專案用 $ npm install react-redux

3. 當我們串在一起:react + redux

先跑 react-create-app 再跑 redux 會比較順,把 style 跟 prettier 那些都裝一裝就開始

引入 redux 的部分,專案結構會長這樣:

  • SRC folder

    • redux folder

      • reducers folder

        • index.js: 利用 combineReducers 把 reducers 放在一起

        • todos.js: todo 相關的邏輯,例如新增、刪除等

        • users.js: 使用者相關的邏輯,例如新增使用者

      • actions.js: action creators 的函式們

      • actionTypes.js: action constants 的規範字元

      • store.js: 創建 store

    • App.js

    • index.js

a. 在 SRC 底下新增 redux 資料夾內的檔案

a.1 先創建 store

// store.js

import { createStore } from "redux";
import rootReducer from "./reducers";

export default createStore(rootReducer);

a.2 建立 reducer todos

// todos.js

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

let todoId = 1

const initialState = {
  todos: []
}

export default function todosReducer(state = initialState, 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
    }
  }
}

a.3 建立 reducer users

// users.js

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

a.3 小優化 action constants(actionTypes.js) & action creator(actions.js)

// actionTypes.js

export const ADD_TODO = "add_todo";
export const DELETE_TODO = "delete_todo";
export const ADD_USER = "add_user";
// actions.js

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,
    }
  }
}

redux 部分結束了,接下來處理其他部分

b. SRC 資料夾底下的 index.js、App.js

b.1 引入 Provider  包起來 + 引入 store,跟 context 有異曲同工之妙

// index.js

import { Provider } from "react-redux";
import store from "./redux/store";

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

b.2 [FE303] > Redux 簡介 > react-redux:套上 React 13'49": 解釋 useSelector

用 hooks 的話,引入 useSelector 可以把 redux 的 state 拿出來,作用是對 store 裡的資料作轉換,把裡面的 reducers 需要的部分拿出來

在 reducers folder 裡面,新增 selectors.js

export const selectTodos = (store) => store.todoState.todos;

// App.js

import { useSelector } from "react-redux";
import { selectTodos } from "./redux/selectors";

export default function App() {
  const state = useSelector(selectTodos);
  console.log('state', state)
  return (
    <div>
      123
    </div>
  );
}

小結:$ npm run start 發現沒有裝 redux 跟 react-redux

b.3 dispatch 去拿想要的東西

// App.js

import { useSelector, useDispatch } from "react-redux";
import { selectTodos } from "./redux/selectors";
import { addTodo } from "./redux/actions";

export default function App() {
  const todos = useSelector(selectTodos);
  const dispatch = useDispatch();
  console.log('state', todos);

  return (
    <div>
      <button onClick={() => {
        dispatch(addTodo(Math.random()))
      }}>add Todo</button>
      <ul>
        {todos.map(todo => <li>{todo.id} {todo.name}</li>)}
      </ul>
    </div>
  );
}

小結:redux 像是一個大老闆,做事都要透過別人,用 select 去倉庫(store)選到東西;用 dispatch 指派任務

4. redux devtool 介紹

在 chrome 有一個擴充功能,先 安裝 並做設定,擴充功能有亮綠色就代表可以用了

我的只有在沒連上線會出現這個訊息:No store found. Make sure to follow [the instructions(https://github.com/zalmoxisus/redux-devtools-extension#usage).

在 store 裡面加上那一行就可以了

// store.js 

import { createStore } from "redux";
import rootReducer from "./reducers";

export default createStore(rootReducer, 
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

5. 在沒有 hooks 以前:connect

參考資料:Connecting the Components

hooks 版本用的是 useSelector & useDispatch,一般更常看到的是 connect(),它是一個函式,用於結合 react component 與 redux

a. connect 可以取代 dispatch,在 App.js 先做調整

// App.js

import { useSelector } from "react-redux";
import { selectTodos } from "./redux/selectors";
import AddTodo from "./AddTodo";

export default function App() {
  const todos = useSelector(selectTodos);
  console.log('state', todos);

  return (
    <div>
      <AddTodo />
      <ul>
        {todos.map(todo => <li>{todo.id} {todo.name}</li>)}
      </ul>
    </div>
  );
}

b. 開一個 AddTodo.js 當作 component,裡面使用 connect 與 redux 做連接

03'02" 開始使用 connect

b.1 connectToStore 會回傳兩個函式

const connectToStore = connect(
  mapStateToProps,
  mapDispatchToProps
)

b.2 設置一下上面那兩個函式

const mapStateToProps = (state) => {
  return {
    todos: store.todoState.todos,
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    addTodo,
  }
}

b.3 用 ConnectedAddTodo 這個新的 component 再去呼叫 connectToStore,串接 redux

const ConnectedAddTodo = connectToStore(AddTodo)

export default ConnectedAddTodo;

小結:這種手法叫做 HOC: higher order component,component 再包一層 component

b.4 mapStateToProps: 回傳 redux 整個 store,你回傳你想要的東西,概念類似 selector

所以像是擁有目錄,下單買單品

b.5 mapDispatchToProps: 派送任務

const mapDispatchToProps = (dispatch) => {
  return {
    addTodo: (payload) => dispatch(addTodo(payload)),
  }
}
// 上面幾行可以簡化成下面的樣子

const mapDispatchToProps = {
  addTodo,
}

小結:

hooks vs connect 的差別

所以說現在這樣寫,跟剛剛 hooks 版本的差別在於對 AddTodo 來說,它不知道 redux 的存在,他只接受一個 addTodo,然後在點下按鈕時去呼叫這個函式

程式界取了一個分類叫做 dumb component,也太壞了吧,這是歧視

相較於最後輸出的 ConnectedAddTodo 這個 component 就叫做 smart component 或者叫它 container,你乾脆叫小聰明算了

慣用手法,化繁為簡,三行變一行

const connectToStore = connect(mapStateToProps, mapDispatchToProps)
const ConnectedAddTodo = connectToStore(AddTodo)
export default ConnectedAddTodo

// 上面三行等於下面一行

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

整理過後,資料夾會長這樣:

  • SRC folder

    • components folder

      • AddTodo.js
    • containers folder

      • AddTodo.js
    • redux folder (內容略)

    • App.js

    • index.js

// App.js 它是老大,從主控者 containers 那邊把 component 拿來用

import AddTodo from "./containers/AddTodo";
// containers 底下的 AddTodo.js 

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

const mapStateToProps = (store) => {
  return {
    todos: store.todoState.todos,
  }
}

const mapDispatchToProps = {
  addTodo,
}

export default connect(mapStateToProps, mapDispatchToProps)(AddTodo);
// components 底下的 AddTodo.js 

export default function AddTodo({ addTodo }) {
  return (
    <button onClick={() => {
      addTodo(Math.random());
    }}>
      add todo
    </button>
  );
}

[FE303] > Redux 簡介 > 在沒有 hooks 以前:connect 15'00":提了一下怎麼用 hooks 實作 container

connect 的優點是測試方便

Dan 哥意見

他寫了一篇文章來比較 dumb(就是 Presentational) vs container component,一開始他推薦分開寫法,但後來他自己補充用 hooks 也可以,因為大家好像太狂熱要分開了(?)

Presentational and Container Components

6. 實作簡易 todo list

通用的狀態再放 redux 就好,當然也可以用 context,但用不好會有效能隱憂

不建議 hooks connect 混寫

a. 新增 todo

// components folder 的 AddTodo.js

import { useState } from "react";

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

b. 刪除 todo

// App.js 

import { useSelector, useDispatch } from "react-redux";
import { selectTodos } from "./redux/selectors";
import AddTodo from "./containers/AddTodo";
import { deleteTodo } from './redux/actions';

export default function App() {
  const todos = useSelector(selectTodos);
  const dispatch = useDispatch();
  return (
    <div>
      <AddTodo />
      <ul>
        {todos.map(todo => 
          <li>
            {todo.id} {todo.name}
            <button onClick={() => dispatch(deleteTodo(todo.id))}>
              delete
            </button>
          </li>
        )}
      </ul>
    </div>
  );
}

老師建議看官方文件練習寫出 todo Tutorial: Using the connect API

結尾想法:

redux 像是大老闆一樣,辦事層層分級,每件事都有傳遞的概念,權責分明;現在所學的東西是在擴展廣度,因為去工作以後,一開始接觸到的比較多是深度吧,我的無知限制了我的想像力;個人比較喜歡 hooks 寫法,純粹是因為 connect 好麻煩










Related Posts

Spring boot系列(二)環境設定

Spring boot系列(二)環境設定

[25] 強制轉型 - 隱含的強制轉型、Addition Operator、Strings <> Numbers

[25] 強制轉型 - 隱含的強制轉型、Addition Operator、Strings <> Numbers

W12_jQuery 筆記

W12_jQuery 筆記


Comments