[Day 22 - Redux] 有了Redux,状态管理没烦恼

前情提要:在前面关於 React 的几篇文章,学会了建立 React Component,依据使用者操作在组件间处理资料。接下来就会提到如何使用 Redux,更方便的来管理应用程序中复杂化的资料状态。

为什麽要使用 Redux ?

在 React 中,State 被用来储存特定的资料状态,网页 UI会根据 State 状态呈现出画面 View,当使用者操作 Action 事件触发资料状态改变,网页 UI 就会依据新的 State 更新网页内容。

若有多个组件需要共享同一个 State,之前虽然提到可以使用提升 State 到最上层 Component,再经过 Props去传递的方法,但当网页内容复杂化,管理的资料状态增加後就很难去控制这些不断变化的 State。

一个很好的解决方法就是将 State 提取到一个地方集中存放,并且指定 State 依据什麽样的 Action 来转变,就可以方便在 Component 间取用共用的资料状态、和操作改变 State 的 Action,这就是 Redux 的作用。

Redux

Redux 是如何运作?在 Redux 中的所有资料都遵循一个单向资料流的模式,当使用者触发操作事件,藉由 Dispatch Action 来让 Store 启动 Reducer,根据当前 State 和指定的 Action,产生一个新的 State 回传,Store 再通知所有相关组件进行对应的更新。

安装 Redux

在 Codesandbox 新增一个 React 专案,在左方的 Dependencies 安装 Redux,不过我们目前还不会用到 React 的任何功能,下一篇才会讲到如何在 React 专案中使用 Redux。

定义 State

在 Redux 中的 State 会是一个单一的物件存放在 Store,每次发生状态更新时,都要参考先前的状态来产生一个新的 State,所以我们需要先定义一个最初始的 State,让网页内容能够依据 State 来产生画面。延续之前的计数器范例来举例,新增 reducer.js 定义 initialState,包含每个成员的姓名和分数资料:

reducer.js

const initialState = {
  members: [
    { name: "May", score: 0 },
    { name: "Julia", score: 0 },
    { name: "Selina", score: 0 }
  ]
};

Action

Action 被用来描述会发生的操作事件,它是一个 JavaScript 物件,包含 type 属性指定 Action 类型、和 payload 属性定义动作所需的额外数据。比如,我们定义一个按下 Plus 按钮的动作 ADD_SCORE,并且取得该成员的编号索引 idx,知道是针对哪位成员的分数进行操作。

// 添加一个新的 action type:
export const ADD_SCORE = "ADD_SCORE";
// 定义一个 action
{
  type: ADD_SCORE,
  payload: { idx }
}

接下来一并定义另一个动作 REDUCE_SCORE,而通常会建立一个 Action Creator 产生该动作的 Function,回传需要的指定 Action 供 Dispatch 使用,如下面的程序码所示:

action.js

export const ADD_SCORE = "ADD_SCORE";
export const REDUCE_SCORE = "REDUCE_SCORE";

// action creator
export function addScore(idx) {
  return {
    type: ADD_SCORE,
    payload: { idx }
  };
}

export function reduceScore(idx) {
  return {
    type: REDUCE_SCORE,
    payload: { idx }
  };
}

Reducer

Reducer 会接收目前的 State 和指定的一个 Action,根据这两个参数计算出下一个 State 并回传给 Store。首先创建一个 Reducer,state 参数的预设值为之前定义的 initialState;传进来的 action 尚未定义时,就回传当前原本的 State:

export default function appReducer(state = initialState, action) {
  switch (action.type) {
    default:
      return state;
  }
}

先来处理 ADD_SCORE 动作,读取 action.typeADD_SCORE 时,执行分数加一的动作,计算出要回传的新 State。这边要注意的是,Reducer 不允许我们直接修改当前的 state,我们只能通过复制该值来修改

reducer.js

import { ADD_SCORE, REDUCE_SCORE } from "./actions";

const initialState...

export default function appReducer(state = initialState, action) {
  switch (action.type) {
    case ADD_SCORE: {
      let new_members = state.members;
      new_members[action.payload.idx] = {
        ...new_members[action.payload.idx],
        score: new_members[action.payload.idx].score + 1
      };
     // 回传一份新的state object
      return {
        members: new_members
      };
    }
    default:
      ...
  }
}

然後是 REDUCE_SCORE 动作的部分:

...
  switch (action.type) {
    case ADD_SCORE: 
        ...
    case REDUCE_SCORE: {
      let new_members = state.members;
      new_members[action.payload.idx] = {
        ...new_members[action.payload.idx],
        score: new_members[action.payload.idx].score - 1
      };
      return {
        members: new_members
      };
    }
    default:
      ...
  }
}

Redux 实际上只会有一个 Reducer 函数,但我们还是可以将 State 拆分给好几个 Reducer 来控制,透过 combineReducers 合并成一个 rootReducer,让 Reducer 只处理与各自相关的 State 更新动作。

const initialState2...

export default function Reducer2(state = initialState2, action) {
  switch (action.type) {
      ...
  }
}

透过 combineReducers 合并组合多个 Reducer:

import { combineReducers } from 'redux'

import reducer1 from ...
import reducer2 from ...

const rootReducer = combineReducers({
  r1: reducer1,
  r2: reducer2
})

export default rootReducer

Store

前面我们创建了 Action 与 Reducer,而 Store 的用处就是将它们组合在一起,保存 State、让网页能够读取更新状态,而每一个 Redux 中只会有一个 Store。接下来的动作就是创建 Store,并引入 rootReducer

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

const store = createStore(rootReducer);

export default store;

Dispatching Actions

建立好 Store 之後,在 index.js 引入 addScorereduceScore 动作,并通过 Store 提供的方法 store.dispatch() 来调用 Action,就能呼叫对应的 Reducer Function 更新 State。

index.js

import { addScore , reduceScore } from "./actions";
import store from "./store";

// 触发addScore,对编号索引 idx = 1 (第二位成员)执行分数+1的动作
store.dispatch(addScore(1));

这样就完成了 Redux 完整的运作,我们另外使用一些 Store 提供的方法来查看更新後的 State 结果。

  • store.getState():取得目前的 State状态
  • store.subscribe(listener):注册 Listener,每次 Dispatch Action 时会呼叫,并且会回传一个用来撤销 Listener 的 Function。

index.js

import { addScore , reduceScore } from "./actions";
import store from "./store";

const unsubscribe = store.subscribe(() =>
  console.log("State after dispatch: ", store.getState())
);

store.dispatch(addScore(1));

// 撤销 listener
unsubscribe();

成功将第二位成员的分数+1


小结

学到了如何在 Redux 透过 Action 管理更新所有的 State,但我们还没有将 State 放到网页元件里渲染出对应的画面,所以接下来,就会谈到如何在 React 中使用 Redux 来处理资料状态,那就下一篇文章再见罗!

范例程序码

如果文章中有错误的地方,要麻烦各位大大不吝赐教;喜欢的话,也要记得帮我按赞订阅喔❤️

参考资料


<<:  【踩坑】按钮闪阿闪,gradient 在 hover 时闪烁

>>:  [Android Studio 30天自我挑战] Switch 元件介绍

作业系统L3-行程

作业系统L3-行程 行程(Process)–正在执行的程序 行程(Process) VS 程序(Pr...

Day04 - Amazon ECS Anywhere 基础说明与建置(下)

先前将主机已经注册上去了 那接下来就是进到『Task Definitions』开始来建立服务 点选『...

如何制作亮丽的Instagram帖子模板

如何制作 Instagram 帖子 3 步曲 从灵感开始 - Visual Paradigm Onl...

Day5 Redis组态档设定-GENERAL 2

Redis.config GENERAL daemonize 是否要用daemon方式启动Redis...

# Day9--老爸,我可以继承你的家产,但我不想长得太像你

引述自100Days of Swift-Class inheritance: The second ...