---
# System prepended metadata

title: Diving into Redux - React compelte guide
tags: ['Javascript, React']

---

# Diving into Redux - React compelte guide

###### tags: `Javascript, React`

# Module Inro

* 會介紹 Redux 這個 lib 是什麼
* 為何他這麼重要
* 以及如何使用它

![](https://i.imgur.com/20Yl869.png)


# Another Look At State In React Apps

> Redux 是一個管理 state 的系統，主要應用在跨元件或是 app-wide 類型的 state 

![](https://i.imgur.com/4wvLDcS.png)

來看看這三種 state 的差異

1. local state

主要 state 的發生以及改變在一個檔案中，例如監聽 user input 或是 toggle 按鈕讓文案開啟\ 關閉，主要在單一個元件中操作 useState, useReducer

2. cross-component state 

跨元件使用 state ，舉例 overlay modal 這種，根據數個不同的元件來決定是否 modal 來執行，就會透過各個不同元件的 props 傳遞來達成

3. app-wide state 

跟上面很像，但是更廣，舉例 authentication state 會根據登入狀態修改全站 UI ，一樣可以透過傳遞 props 來達成

![](https://i.imgur.com/MaawAtU.png)


基於 2, 3 點要到處傳遞 props 相當麻煩，所以衍生出了 React Context API 以及現在要介紹的 Redux ，不過既然都有 Context API 為何 Redux 還會出現呢？

# Redux vs React Context

> 會有 Redux 的出現自然是因為 Context 有其缺點

## 注意！

**context API 跟 Redux 是可以同時出現的，Redux 控管整體的程式 state ，部分幾個 component 可以使用 context API 各別控制 state** 

## 缺點

使用 context API 會因為有各種不同的 state 要傳遞，因此需要建立複數的 provider 來供應這樣的需求，就會變成這種情況，尤其是當 app 相當大的時候

![](https://i.imgur.com/r4N7v8P.png)

不過這時候你會想，這樣把全部的 state 全部寫在同一個 provider 就好啦？

但會導致扣又肥又大，非常不好維護

![](https://i.imgur.com/c9j0Qr6.png)

一篇來自 React 開發者對於 context API 的描述在於，對於 state 更新率高過一般的的情況下，其效能會比較差

![](https://i.imgur.com/BRFVlOK.png)

# How Redux Works

> central data 不會直接被 component 改動，必須透過 action 告訴 Reducer component 想做什麼， Reducer 內處理好後在覆蓋掉 central data ，然後 central 就會告訴訂閱的 components 需要改動的 UI

![](https://i.imgur.com/JL6paTV.png)


# Exploring The Core Redux Concepts

> 這邊會簡單操作一個 app 每次使用 action 後會使 state +1 的程式，並且操作 node 來執行

## 注意!

reducer function 一定會是 pure function

![](https://i.imgur.com/BOOMdbn.png)


pure function 代表： 放入一樣的 input ，輸出的的結果每一次都會是一樣的 output ，並且不會有 side effect 

1. 建立 store  操作 createStore 並且帶入參數 reducer 
2. 建立 reducer function，他會吃兩個參數： state, action 並且返回 state 來更新 store 
3. 建立 counterSubscriber 這邊是在模擬 UI component 它會收到 store 的 state 來更新 UI
4. 將設置好的 subscriber 跟 store 連結使用 store 內的函式 subscribe
5. 定義 action 名稱並且使用 store 內的函式 dispatch 來帶入


```javascript=
const redux = require('redux'); // 範例使用 node 所以這樣來引入 redux 
const countReducer = (state = { counter: 0}, action) => { // 這邊 counter 要給預設值，設置為 0 
  return { counter:state.counter +1, }; // return +1，因此初始值為 1
};

const store = redux.createStore(countReducer);// 裡面的參數會是 reducer func
const counterSubscriber = () => {
  const latestState = store.getState(); // 是 createStore 內建的函式，用途在於取得最新的 state
  
  console.log(latestState); // 這邊是 node 執行的內容
};

store.subscribe(counterSubscriber);
store.dispatch({type: 'increment'}); // 這邊使用 dispatch 來執行特定 type 的動作
```
這邊印出直接 increment 後的內容也就是 2 !(畫面已經更新)

![](https://i.imgur.com/PSJ8Ipn.png)

# More Redux Basics

> 一般 reducer function 中都會處理複數情況的 action 

其實也不複雜就是加上判斷式針對 type 去做辨別即可，如果都沒有符合的 action 則 return 原本的 state 

```javascript=
const countReducer = (state = { counter: 0}, action) => {

  if (action.type ==='increment'){
    return { counter:state.counter +1, }; 
  } else if( action.type === 'decrement') {
    return { counter:state.counter -1, };
  } else {
    return state;
  }
};
```

# Preparing a new Project

> 這邊要把上面的加減的 app 搬上去 react 操作

要特別注意的是在安裝 redux 時，如果是操作在 react 上要記得加上 react-redux

`npm install redux react-redux`

# Creating a Redux Store for React

* 下方的扣可以看出跟 node 明顯的不同在於， import redux 的方式以及因為要在其他元件使用 store 所以必須 export 出去
* 另一點是不需要使用 subscribe 以及 dispatch 函式（因為牽涉到其他元件，不會只在這邊操作）

這樣子 store 的內容就建立好了

```javascript=
import { createStore } from 'redux';

const countReducer = (state = { counter: 0}, action) => {

  if (action.type ==='increment'){
    return { counter:state.counter +1, }; 
  } else if( action.type === 'decrement') {
    return { counter:state.counter -1, };
  } else {
    return state;
  }
};

const store = createStore(countReducer);

export default store;
```
 
# Providing the Store

> 這邊的動作跟 context API 非常像，使用 provider 包裹最外層的 app 來傳遞所有的 props 以及操作 action 並且要記得綁定 store 當作 props

最外層的 index.js

```javascript=
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import store from "./store/index";

import "./index.css";
import App from "./App";

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);
```
# Using Redux Data in React Components

> 這邊要做的事情就是解釋 subscribe 在 react 中的行為

要把 store 中的內容呈現到 UI 剛剛解釋說得使用 subscribe ，但是使用 react 則是這樣操作：

* import useSelector 用來選取一小部分需要的 state 
* 並且指派給變數後就可以操作那個 state 進去 jsx 中

如此一來這個 Counter.js 就跟 store subscribe 好，並且可以把 state 的數字呈現到頁面上摟

![](https://i.imgur.com/AZ379ww.png)

```javascript=
import classes from './Counter.module.css';
import { useSelector } from 'react-redux';

const Counter = () => {
  const toggleCounterHandler = () => {};

  const counterValue = useSelector( state => state.counter);
  return (
    <main className={classes.counter}>
      <h1>Redux Counter</h1>
      <div className={classes.value}>{counterValue}</div>
      <button onClick={toggleCounterHandler}>Toggle Counter</button>
    </main>
  );
};

export default Counter;
```

# Dispatching Actions From Inside Components

> 這邊實際來操作 dispatch function 

* dispatch 並不會攜帶內容進來元件，但它需要執行並且指派給變數做操作
* 接下來就是帶入 onClick 指向的函式，並且輸入 type 名稱即可
* 最後寫進去 jsx 中的 onClick 即可，畢竟 UI 的部分就是點擊可以做到加減的動作

```javascript=
const dispatch = useDispatch();

  const incrementHandler = () => {
    dispatch({type: 'increment'});
  };

  const decrementHandler = () => {
    dispatch({type: 'decrement'});
  };
```

# Redux with Class-based Components

> 直接把上面的 FC 改寫成 class components 

* 首先引入 component 來使用 extends 創建 class Counter
* render 的部分操作 JSX 跟 FC 部分是一樣的
* 使用到的 methods 操作方式如下


最重要的部分是：
* connect 如何操作

connect 會帶入兩個參數並且他會再返回一個函式，它會直接執行，其參數為 class component，connect 的兩參數為：一是取得 store 的函式，另一為 dispatch 的函式

connect 也一樣會以 subscribe 的作用

* 取得 store 內的 props 的方法

基本上跟 FC 一樣寫入 state ，並且給予 key name 後帶入對應的 state 中的值

* 以及 dispatch 的寫法

dispatch 的部分則是先給予 key name 後，執行匿名函式在其中執行 dispatch 

**要特別注意 JSX 中的值都修改成使用 this 以及 props 的方式帶入， onClick 執行函式的部分則是要記得 bind this 上去確保其 this 的指向正確**

```javascript=
import classes from "./Counter.module.css";
import { connect } from "react-redux";
import { Component } from 'react';

class Counter extends Component {
  incrementHandler () {
    this.props.increment();
  }

  decrementHandler () {
    this.props.decrement();
  }

  toggleCounterHandler () {}

  render () {
    return (
      <main className={classes.counter}>
      <h1>Redux Counter</h1> 
      <div className={classes.value}>{this.props.counter}</div>
      <div>
        <button onClick={this.incrementHandler.bind(this)}>Increment</button>
        <button onClick={this.decrementHandler.bind(this)}>Decrement</button>
      </div>
      <button onClick={this.toggleCounterHandler}>Toggle Counter</button>
    </main>
    );
  }
}

const mapStateToProps = state => {
  return {
    counter: state.counter
  };
}

const mapDispatchToProps = dispatch => {
  return {
    increment: () => dispatch({type: 'increment'}), // 這邊的寫法也要注意
    decrement: () => dispatch({type: 'decrement'}),
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(Counter); // 這邊是最特別的地方

```
 
# Attaching Payloads to Actions

> 這邊要解釋如何使用 payloads ，像是 action 的參數的概念

在 dispatch type 的後方加入 payload ，名字是可以自定義的，雖然這邊 payload 是寫死的，不過一般情況下，可以透過 useState, useRef 直接輸入就可動態的帶入使用者的 payload 摟！

```javascript=
const incrementHandler = () => {
    dispatch({type: 'increment', amount: 10});
  };
```
並且在 redux 中操作的時候，使用即可相當簡易

```javascript=
if (action.type ==='increment'){
    return { counter: state.counter + action.amount}; 
  } 
```

![](https://i.imgur.com/mFVz9yy.png)


# Working with Multiple State Properties

> 這邊會多操作一種 state 來演示複數的 state 的情況下，如何在 redux 中操作

> 操作 toggle counter 讓按鈕點擊後，隱藏數字，雖然這個 state 相當單純甚至可以使用 local state 操作就好，不過基於 demo 需求，所以把它操作成 global state 來作用在 redux 內

這邊演示 store.js 內的操作，當新的 state showCounter 出現的時候，因為 Reducer 操作的方式是將 action 的操作覆蓋掉原本的 state ，因此內部的操作都必須加上 showCounter 儘管該 action 沒有關聯

```javascript=
if (action.type === 'decrement') {
    return {
      counter: state.counter - 1,
      showCounter: state.showCounter // 儘管沒有關聯但是因為複數的 state ，這邊也需要加上
    };
  }

  if (action.type === 'toggle') {
    return {
      showCounter: !state.showCounter, // 儘管沒有關聯但是因為複數的 state ，這邊也需要加上
      counter: state.counter
    };
  }
```

在 Counter.js 操作第二個 state 一樣必須使用 useSelector 來抓取 state 後，dispatch 操作即可

```javascript=
const show = useSelector((state) => state.showCounter
                         
const toggleCounterHandler = () => {
    dispatch({ type: 'toggle' });
  };


// 記得 JSX 的部分必須這樣條件操作來開關數字

  return (
{show && <div className={classes.value}>{counter}</div>}
      )
```

就可以 toggle 數字摟！

![](https://i.imgur.com/b58ETtz.png)


# How To Work With Redux State Correctly


> 影片開頭提到一個重點 redux 的 Reducer 最後一定會回傳一個 state 來取代目前最新的 state ，但是要非常注意，**它並不會 merge 原本的 state 它做的是取代**！！非常重要

第一個重點：

**redux 的 Reducer 回傳的 state 它並不會 merge 原本的 state 它做的是取代！！**

舉個例子：

假設 Reducer 中的 increment 我們忘記加上去了 showCounter 會發生什麼事呢？

```javascript=
if (action.type === 'increment') {
    return {
      counter: state.counter + 1,
    };
  }
```

它會覆蓋掉原來的 state ，變成 showCounter 永遠消失，因此 show 永遠都是 undefined 所以這樣的 side effect 就會讓數字從此不再出現

第二個重點：

**千萬不要 mutate Reducer 內的 state**

因為 物件或是陣列是傳址的特性，容易發生問題而找不到地方解決！

傳址 (referrence type) 很容易造成，當你修改其屬性，因為地址相同導致，你改了內容其他地方的內容也跟著修改

例子：

```javascript=

// 錯誤範例
if (action.type === 'increment') {
    state.counter ++
    
    return state
  }
  
// 錯誤範例
  if (action.type === 'increment') {
    state.counter ++
      
    return {
      counter: state.counter,
      showCounter: state.showCounter
    };
  }

// 正確做法 操作在 return 的物件內，讓原本的 state 保持原樣

if (action.type === 'increment') {
    return {
      counter: state.counter + 1,
      showCounter: state.showCounter
    };
  }
```

[影片參考 - Reference vs Primitive Values](https://academind.com/tutorials/reference-vs-primitive-values)

# Redux Challenges & Introducing Redux Toolkit

> 接下來會介紹到 Redux Toolkit 來解決一些問題，不過會先介紹單純使用 Redux 的情況下並且程式使用的 state 用來越多，會遇到的挑戰

1. type 名稱不能打錯，不然絕對報錯

當然程式規模小不成問題，但是當多人協作，且規模擴大，這種情況就很可能發生

他是可以使用 JS 設定變數的方式來帶入，並且輸出到其他地方，不過下面會介紹更好的方式！

2. 剛剛有提到複數的 state ，每增加一個 steta 就必須所有的 type 都增加進去，程式才不會壞掉

這個情況當 state 越來越多，每一個 return 的 state 只會越來越長，似乎跟 Context API 一樣？不過其實他是有解的！待會再說
 
3. 還記得 Reducer 內的 state 不能 mutate 嗎？

但是當 state 愈來越多且複雜，有時候不是我們不想，是情況難以控制呀！

這邊 Redux toolkit 來救場啦！

他其實是 React redux 同一個團隊製造的工具，在下個章節中會開始介紹它！

# Adding State Slices

`npm i @reduxjs/toolkit` 先載個 toolkit ～

引入 createSlice 後來操作

1. 首先他會需要一個 name 
1. 再來使用初始值
1. 接下來輸入 reducers 

```javascript=
const counterSlice = createSlice({
  name:'counter',
  initialState,
  reducers: {
    increment(state) {
      state.counter++;
    },
    decrement(state) {
      state.counter--;
    },
    increase (state, action) {
      state.counter = state.counter + action.amount;
    },
    toggleCounter(state) {
      state.showCounter = !state.showCounter;
    }
  }
})
```

跟 redux 最大的差異在於：

* 不需要根據 type 去寫 if/else
* 可以 mutate state ，但是其實在底層有做處理實際上 state 還是沒有被 mutate 
* 可以分多個資料夾去處理不同類別的 state
 
# Connecting Redux Toolkit State

> 這邊我們會操作 configureStore 來取代 createStore 

它的功能在於一樣會創造一個 store 之外，並且結合多個不同的 slice 的 reducers 變成一個 reducer 會負責 global state 

```javascript=
const store = configureStore({
  reducer: {counter: counterSlice.reducer} // 這邊如果有複數的 reducers 就可以在這個物件內新增下去，在底層的機制還是會把內部的 reducers 合併成一個輸出出去
});
```
# Migrating Everything To Redux Toolkit

來看看整個 refactor 過後的扣，簡潔很多

action 的部分會輸出到需要使用的資料夾即可使用

```javascript=
import { createSlice, configureStore } from '@reduxjs/toolkit';

const initialState = { counter: 0, showCounter: true };

const counterSlice = createSlice({
  name:'counter',
  initialState,
  reducers: {
    increment(state) {
      state.counter++;
    },
    decrement(state) {
      state.counter--;
    },
    increase (state, action) {
      state.counter = state.counter + action.payload; // 這邊的 payload 會是其預設值，不能向 redux 一樣可以自訂
    },
    toggleCounter(state) {
      state.showCounter = !state.showCounter;
    }
  }
})

const store = configureStore({
  reducer: counterSlice.reducer // 這邊要注意他的 reducer 不加 s 
});

export const counterAction = counterSlice.actions;

export default store;
```

使用 actions 的方式，就直接取用 counterAction 底下的 methods 即可，帶入的參數會以 payload 的方式被帶入 reducer 內使用

```javascript=
const increaseHandler = () => {
    dispatch(counterAction.increase(10));
  };
```
 
# Working with Multiple Slices

> 剛剛學到的都是只有單一條 slice 且 state 很少的情況下，那如果有多條 slices 呢？

這邊會用一個登入狀態來解釋 global 的 state 如何呈現：

* 比方說那個 log out 按鈕只有在登入狀態下才會出現

![](https://i.imgur.com/JaOIXyR.png)

其實步驟目前看起來跟只有 counterSlice 差不多

重點差異在於：

configureStore 內的因為多了一個 slice 它的寫法不太一樣了，變成需要有一個辨別 slice 的 identifier 在前面辨別，這邊要特別注意，**所以在 useSelector 的地方 state 的抓取會多一層**


這邊以 Counter 為例子，可以看到有兩層才能擷取到 state 
![](https://i.imgur.com/ntIsFFW.png)


```javascript=
const initialAuthState = {
  isAuthenticated: false
};

const authSlice = createSlice({
  name: 'authentication',
  initialState: initialAuthState,
  reducers: {
    login(state) {
      state.isAuthenticated = true;
    },
    logout(state) {
      state.isAuthenticated = false;
    }
  }
})

const store = configureStore({
  reducer: {
    counter: counterSlice.reducer,
    auth: authSlice.reducer
  }
}); 

export const counterAction = counterSlice.actions;
export const authAction = authSlice.actions;
```

# Reading & Dispatching From A New Slice

> 這邊會根據這個 login 的 state 去做到條件篩選 ＵＩ

以 App.js 舉例

* 首先抓出 isAuthenticated 來辦別登入與否
* 接下來在 JSX 中條件篩選 UI 來呈現


未登入
![](https://i.imgur.com/051Hh28.png)

已登入
![](https://i.imgur.com/y6Lng0C.png)

App.js

```javascript=
function App() {

  const isAuth = useSelector(state => state.auth.isAuthenticated);

  return (
    <>
    <Header/>
    {!isAuth && <Auth/>} // 如果沒登入就顯示輸入帳號欄位，登入就讓其消失
    {isAuth && <UserProfile/>} // 如果登入就顯示 user 帳號內容，沒有則消失
    <Counter />
    </>
  );
}
```

dispatch 的部分則是操作跟之前的一樣，引入 authAction 之後就可以使用它內部寫好的方法摟！

Header.js

```javascript=
const Header = () => {
  const isAuth = useSelector((state) => state.auth.isAuthenticated);

  const dispatch = useDispatch();

  const logOutHandler = () => {
    dispatch(authAction.logout());
  }
  
  return (...省略)
  }
```

# Splitting Our Code

> 隨著 slices 越來越多 store.js 檔案越來越大，因此我們來拆分他們

原本都放在 store/index.js 內，這邊把兩種 slices 拆分成 auth, counter 

![](https://i.imgur.com/JNiXNhx.png)

拆完之後的扣非常乾淨！

store/index.js

```javascript=
import {
  configureStore
} from '@reduxjs/toolkit';

import counterReducer from './counter';
import authReducer from './auth';

const store = configureStore({
  reducer: {
    counter: counterReducer,
    auth: authReducer
  }
}); 

export default store;
```
slices 的部分使用 auth.js 做範例

* 基本上拆出 initialState
* 以及整個 slice 的身體
* 記得操作 actions
* 最後輸出 reducer 的部分（如果有使用到其他的部分也可以整個 authSlice 輸出

```javascript=
import {
  createSlice
} from '@reduxjs/toolkit';

const initialAuthState = {
  isAuthenticated: false
};

const authSlice = createSlice({
  name: 'authentication',
  initialState: initialAuthState,
  reducers: {
    login(state) {
      state.isAuthenticated = true;
    },
    logout(state) {
      state.isAuthenticated = false;
    }
  }
});

export const authAction = authSlice.actions;
export default authSlice.reducer;
```
