Redux และ React
[ สามารถดู video ของหัวข้อนี้ก่อนได้ ดู video
Redux คืออะไร
Redux คือ open source Javascript library ที่ใช้สำหรับจัดการ state ภายใน application โดยเป็นการนำ state มาไว้เป็นศูนย์กลางของการเก็บข้อมูลแทน โดยปกติจะใช้ร่วมกับ Single page web applications (SPA) และบ่อยครั้งก็ใช้ร่วมกับ framework อย่าง React ที่แม้ React จะมี library หลายตัว support ก็ตาม แต่ Redux ก็ยังคงเป็น 1 library ที่ยังคงสามารถจัดการ State ได้ดีเช่นกัน
Idea ใหญ่ๆของ Redux คือการจัดการ state ภายใน application โดยจะเป็นตัวทั้งจัดการและ update state ทั้งหมดภายใน application เพื่อให้ state นั้นเกิดความ consistent กันทั้ง application ออกมาได้ (ให้อารมณ์เหมือนมีคนจัดการข้อมูลอยู่ตรงศูนย์กลาง และเมื่อมีการเรียกใช้หรือแก้ไขข้อมูลอะไร ก็จะต้องแก้ข้อมูลจากศูนย์กลางนั้น) โดย core priciples หลักๆของ Redux จะมีประมาณนี้
- Single Source of Truth โดย application อะไรก็ตามที่ใช้ Redux ทั้ง application จะเปลี่ยนมาจัดการ state ผ่าน redux โดยใช้ Javascript object ที่เรียกกันว่า
storeเพื่อให้ทั้ง application มีศูนย์กลางในการจัดการข้อมูลเพียงที่เดียว และมีข้อมูลเพียงชุดเดียวที่ใช้สำหรับการจัดการ application ออกมาได้ - State ที่ถูกสร้างมาเพื่อ Read-Only โดยใน Redux นั้นจะไม่สามารถแก้ไข state ของ redux (store) โดยตรงได้ โดยจะต้องทำผ่านการส่ง action (dispatch) เพื่อเป็นการสื่อสารไปยัง store ว่าจะขอแก้ไข และจัดการผ่าน Reducer ที่ทำหน้าที่ในการ handle action ที่ส่งเข้ามาและทำการ modified เป็น state ใหม่ออกมาแทนการแก้ไข store ตรงๆออกมาได้ = ส่งผลทำให้มีเพียงแค่การส่ง action เท่านั้นที่สามารถแก้ไข store ได้ ทำให้ code ระหว่าง store และ component แยกส่วนในการจัดการออกจากกันได้
- ให้การเปลี่ยนแปลงเป็น Pure function โดย Reducers นั้นถูก design ให้มาเป็น pure function ที่จะทำการนำ state ปัจจุบัน และ action ทางเข้าเป็น input เข้ามาใหม่เพื่อนำข้อมูลมาทำเป็น state ใหม่โดยไม่จำเป็นต้องแก้ไข state ปัจจุบันได้
- รวมถึง มีอุปกรณ์ที่สามารถ Debug การเดินทางของข้อมูลผ่ าน Redux's architecture ได้ โดยสามารดูจากการใช้เครื่องมือ record กับ replay action ออกมาได้
โดยการใช้ Redux นั้นจะเป็นการกำหนด
- action เพื่ออธิบายว่าระบบนี้สามารถทำอะไรได้บ้าง (action คือสิ่งที่สามารถแก้ไข store ได้)
- reducer ที่สามารถระบุได้ว่า state สามารถเปลี่ยนแปลงไปยังไงได้บ้างจาก action
- store เพื่อทำการเก็บ application state เอาไว้
โดยการใช้ redux นั้นสามารถช่วยในการจัดการ application ที่มีการจัดการ state ที่ซับซ้อนได้ โดยการให้ state รวมจัดการที่ store ที่ดียวกัน และจัดการ application ภายในที่เดียวกันได้ ซึ่งนี่คือข้อดีเทียบกับการใช้ state แยกแต่ละ Component ทำให้การ debug state ของ application มาอยู่ที่ Redux ที่เดียวได้นั่นเอง
องค์ประกอบของ Redux
เพื่อให้เป็นภาพชัดมากขึ้นเรื่อง store, action, reducer เรามาดูภาพรวมองค์ประกอบของ Redux กันก่อน
จาก diagram นี้เรามารู้จักแต่ละองค์ประกอบแบบไล่ flow ไปกัน
1. Store และ Reducer
เริ่มต้นที่ Store และ Reducer กันก่อน
- Store คือส่วนที่เก็บข้อมูลไว้เป็นส่วนกลางเป็น Javascript Object ที่ทำการเก็บ application state เอาไว้ โดยใน Redux นั้นจะมีเพียงแค่ store เดียวที่ทำการเก็บ state ทั้งหมดของ appilcation ไว้ โดย store นั้นจะโดนส่งต่อไปยัง reducer เพื่อให้ reducer สามารถจัดการร่วมกับ action ต่อได้ รวมถึงสามารถ subscribe เพื่อตรวจจับ change ที่เกิดขึ้นใน state ปัจจุบันได้ โดย store นั้นมีหน้าที่ในการรวม action และ reducer เข้าด้วยกัน
- Reducer คือ pure function ที่นำ state ปัจจุบันของ application และข้อมูลที่ส่งผ่าน action เข้ามา (โดยปกติจะเรียกว่า payload) เพื่อนำมา return กัลบมาเป็น state ใหม่ออกมาได้ โดย reducer นั้นทำหน้าที่ในการจัดการเปลี่ย น state ตาม action ที่กำหนดไว้ใน reducer ได้ โดย Reducer นั้นต้องเป็น pure function เพื่อไม่ให้เกิด site effect จากการดึงข้อมูลภายนอกมาใช้ได้ (ต้องป้องกันไม่ให้มีการ modified state โดยตรง)
เพื่อให้เห็นภาพมากขึ้นเรามาดูผ่าน code ตัวอย่างของ store และ reducer กัน
// reducers/counterReducer.js
import { INCREMENT, DECREMENT, SET_COUNT } from "../actions/counterActions";
const initialState = {
count: 0,
};
const counterReducer = (state = initialState, action) => {
switch (action.type) {
case INCREMENT:
return {
...state,
count: state.count + 1,
};
case DECREMENT:
return {
...state,
count: state.count - 1,
};
case SET_COUNT:
return {
...state,
count: action.payload, // Set the count directly from the action's payload
};
default:
return state;
}
};
export default counterReducer;
จาก code ด้านบนนี่ เรามีการประกาศใช้ counterReducer สำหรับ Redux store ที่ใช้สำหรับจัดการ state ของ counter ภายใน application ตัวนี้ เมื่อเราลอง break down แต่ละส่วนออกมา จะมีองค์ประกอบตามนี้
- มี
initialStateตั้งค่าสถานะเริ่มต้นของ counter โดยมีการกำหนดcountเริ่มต้นที่0โดย state นี้จะถูกใช้จากการที่ reducer ถูกเรียกครั้งแรกและทำการส่งออกเป็นจาก default ของ reducer ออกมา (เป็น state เริ่มต้นของ counter ใน application) - มี Reducer function
counterReducerเป็นการกำหนดว่า state สามารถเปลี่ยนแปลงยังไงได้บ้างจาก action ที่ส่งเข้ามา เพื่อเปลี่นแปลงค่าต่อไปยัง store โดยstateนั้นคือ state ปัจจุบันของ application และ การ return คือการส่ง state ใหม่โดยแยกตามเคสของ action แต่ละตัวออกมา - อย่างที่เห็นใน Reducer เราจะมีการแยก case แต่ละ action ออกมา โดย
actionคือ object ที่นำเสนอถึง action ที่สามารถส่งเข้ามายัง reducer ได้ โดยactionจะต้องกำหนดtype(ประเภทของ action ที่ส่งเข้ามา) และpayload(ข้อมูลที่ส่งเข้ามาคู่กับ action) - โดย
switch caseนั้น เราจะเรียกว่า Action Handling คือการใช้ switch ในการแยกเส้นทางของ action เพื่อเป็นการกำหนดการ update state ออกมา โดยตัวอย่างจากในเคสนี้คือ- action
INCREMENTสำหรับ เพิ่ม count ใน state ไป 1 - action
DECREMENTสำหรับ ลด count ใน state ไป 1 - action
SET_COUNTสำหรับ update count ตามค่าที่ส่งมาผ่านpayloadใน action
- action
- โดยเพื่อให้ action นั้นมีการจัดการจากที่เดียวกัน ทั้งจากฝั่งแยก case ของ action (ที่ Reducer ใช้) และ case ของ component (ฝั่งที่ส่ง action เข้ามาที่ reducer) เราจะมีการสร้างไฟล์ action แยกออกมา เพื่อให้ action มี spec ที่ตรงกันได้ ผ่านไฟล์
counterAction.js(เดี๋ยวเราจะพูดในหัวข้อต่อไป)
และ ท้ายที่สุดเมื่อทำการสร้าง reducer เรียบร้อย เราจะต้องนำ reducer นั้นส่งไปยัง process ของการสร้าง store โดยเราจะทำการส่ง counterReducer ไป register store ผ่าน คำสั่ง createStore ใน Redux เช่นแบบนี้
import { createStore } from "redux";
import counterReducer from "./reducers/counterReducer";
const store = createStore(counterReducer);
คำสั่งนี้ก็จะเป็นคำสั่งที่สามารถทำให้เรียกใช้ store counter จาก Redux ได้
2. Actions
Action คือ Javascript object ที่นำเสนอ intention (ระบุสิ่งที่จะทำ) ของการเปลี่ยนแปลง state ของ application โดย Action นั้นจะต้องประกอบด้วย
typeเพื่อเป็นการบอกประเภทของ action ที่จะทำ (intention) โดยจะเป็นการระบุเป็น string constantspayload(optional) คือ data หรือข้อมูลที่จะส่งร่วมกับ action ประเภทนั้น โดย Action เป็น “ทางเดียว” ที่สามารถส่ง data เข้าไปยัง store ได้ (อย่างที่บอก store จะไม่มีวิธีแก้ไขโดยตรงได้นอกเหนือจากการส่งผ่าน action) โดย payload นั้นจะเป็นข้อมูลประกอบเพื่อให้สามารถแปลง store เป็นยัง state ต่อไปได้
นี่คือตัวอย่าง code ของ Action
// actions/counterActions.js
// Existing Action Types
export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";
// New Action Type
export const SET_COUNT = "SET_COUNT";
// Existing Action Creators
export const increment = () => ({
type: INCREMENT,
});
export const decrement = () => ({
type: DECREMENT,
});
// New Action Creator with Payload
export const setCount = (count) => ({
type: SET_COUNT,
payload: count,
});
องค์ประกอบของแต่ละส่วนประก อบด้วย
- Action Types
- จุดประสงค์ ประเภทของ Action คือค่า constant แทนชนิดของ Action ที่สามารถส่งไปยัง Redux store เพื่อกระตุ้นให้เกิดการเปลี่ยนแปลง state ของ Redux ซึ่งช่วยทำให้ชื่อ Action ต่างๆ เป็นไปในทางเดียวกัน และลดความเสี่ยงที่จะพิมพ์ชื่อ Action ผิดในหลายๆ ส่วนของ application
- โดยประเภทของ Action ที่กำหนดไว้ใน code นี้จะมี
INCREMENTแทนประเภทของ Action ที่ใช้ในการเพิ่มค่า counterDECREMENTแทนประเภทของ Action ที่ใช้ในการลดค่า counterSET_COUNTประเภทของ Action ใหม่สำหรับกำหนดค่า counter ให้มีค่าเป็นตัวเลขตามที่ส่งค่าเข้ามา
2. Action Creators
- จุดประสงค์ Action Creator เป็น function ที่สร้างและ return Object ของ Action จากนั้น Object เหล่านี้จะถูกส่งไปยัง Redux store เพื่อกระตุ้นให้เกิดการเปลี่ยนแปลง state ขึ้นมาใน Reducer
- โดย function ที่มีจาก code นี้จะมี
increment()สร้าง Action เพื่อเพิ่มค่า counter และ return Object ของ Action ที่มีประเภท INCREMENTdecrement()สร้าง Action เพื่อลดค่า counter และ return ของ Action ที่มีประเภทเป็น DECREMENTsetCount(count)สร้าง Action เพื่อกำหนดค่า counter เป็นตัวเลขตามที่ส่งค่าเข้ามา (ซึ่งก็คือ count ใน parameter) และ return object ของ Action ที่มีประเภทเป็น SET_COUNT และมี payload เป็นค่า count ที่ส่งเข้ามา
- ท้ายที่สุด return ของ Action Creator ก็จะโดนส่งต่อไปยัง function ของ Reducer และทำงานต่อใน Reducer ได้
3. Dispatch
dispatch ที่มีอยู่บน store นั้นทำหน้าที่เหมือน "พนักงานส่งข้อความ" คอยส่ง "คำสั่ง" (action) ไปยัง store โดยคำสั่งเหล่านี้เป็นเพียง Object JavaScript ธรรมดาที่บอกให้ application เปลี่ยนแปลงสถานะตัวเอง
พูดอีกอย่างคือ dispatch คือ "ช่องทางเดียว" ที่ทำให้ state ของ application Redux เปลี่ยนแปลงได้
เมื่อมีคำสั่งถูกส่งออกไป (dispatch) Redux จะส่งคำสั่งนั้นไปยังฟังก์ชัน reducer ที่เกี่ยวข้องกับ store โดย reducer จะทำหน้าที่ตัดสินใจว่าจะเปลี่ยนสถานะอย่างไร ขึ้นอยู่กับ "ประเภท" ของคำสั่ง (ที่มีการ handle ผ่าน switch case ใน reducer ไว้และบางครั้งอาจขึ้นอยู่กับข้อมูลอื่นๆ ในคำสั่งด้วย เช่น payload)
สิ่งสำคัญคือ สถานะของ Redux นั้นเป็นแบบ immutable ไม่สามารถแก้ไขโดยตรง ดังนั้น reducer จะต้องสร้างอ็อบ Object ขึ้นมาแทนที่ของเดิมเสมอ เพื่อแสดงถึงสถานะใหม่ของ application
เปรียบเทียบง่ายๆ คิดว่า dispatch เหมือนกับพนักงานส่งคำสั่งไปยังแผนกต่างๆ ในบริษัท แผนกเหล่านั้น (reducer) ก็จะรับคำสั่งไปปรับเปลี่ยนวิธีการทำงานของตัวเองตามความเหมาะสม และผลลัพธ์สุดท้ายจะสะท้อนออกมาเป็นการเปลี่ยนแปลงของบริษัทโดยรวม (สถานะทั้งหมดของ application)
นี่คือ code ตัวอย่างของ dispatch
// components/Counter.js
import React, { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { increment, decrement, setCount } from "../actions/counterActions";
const Counter = () => {
const count = useSelector((state) => state.count);
const dispatch = useDispatch();
const [newCount, setNewCount] = useState(0); // Local state to hold the input value
return (
<div>
<h2>Counter: {count}</h2>
<button onClick={() => dispatch(increment())}>Increment</button>
<button onClick={() => dispatch(decrement())}>Decrement</button>
<div>
<input
type="number"
value={newCount}
onChange={(e) => setNewCount(Number(e.target.value))}
/>
<button onClick={() => dispatch(setCount(newCount))}>Set Count</button>
</div>
</div>
);
};
export default Counter;
code นี้เป็นตัวอย่างการใช้งาน Redux ใน React เพื่อจัดการ state ของ counter ซึ่งเป็นส่วนประกอบที่แสดงจำนวนนับ (count) ที่มีความสามารถในการเพิ่ม ลด และตั้งค่าจำนวนนับตามค่าที่ผู้ใช้กำหนด โดย
- มี
useSelectorใช้เพื่อเข้าถึงสถานะใน store ของ Redux ในตัวอย่างนี้ถูกใช้เพื่ออ่านค่าcountจา ก state ปัจจุบันเข้ามาที่ Component - มี
useDispatchfunction ที่ให้ใช้ในการส่ง action (dispatch) ไปยัง store ของ Redux ซึ่งในที่นี้จะใช้เพื่อเรียก action ทั้ง 3 ตัว (เหมือนกับตัวอย่างก่อนหน้า) คือincrement,decrement, และsetCount - เมื่อผู้ใช้คลิกปุ่ม "Increment" หรือ "Decrement"
dispatchจะถูกเรียกพร้อมกับ actionincrement()หรือdecrement()ตามลำดับ ซึ่งจะส่ง action ไปยัง store และทำให้ค่าcountใน state เพิ่มขึ้นหรือลดลง (จาก reducer)