Redux และ React
/ 18 min read
สามารถดู 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 กัน
จาก 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 เช่นแบบนี้
คำสั่งนี้ก็จะเป็นคำสั่งที่สามารถทำให้เรียกใช้ 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
องค์ประกอบของแต่ละส่วนประกอบด้วย
- 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
code นี้เป็นตัวอย่างการใช้งาน Redux ใน React เพื่อจัดการ state ของ counter ซึ่งเป็นส่วนประกอบที่แสดงจำนวนนับ (count) ที่มีความสามารถในการเพิ่ม ลด และตั้งค่าจำนวนนับตามค่าที่ผู้ใช้กำหนด โดย
- มี
useSelector
ใช้เพื่อเข้าถึงสถานะใน store ของ Redux ในตัวอย่างนี้ถูกใช้เพื่ออ่านค่าcount
จา ก state ปัจจุบันเข้ามาที่ Component - มี
useDispatch
function ที่ให้ใช้ในการส่ง action (dispatch) ไปยัง store ของ Redux ซึ่งในที่นี้จะใช้เพื่อเรียก action ทั้ง 3 ตัว (เหมือนกับตัวอย่างก่อนหน้า) คือincrement
,decrement
, และsetCount
- เมื่อผู้ใช้คลิกปุ่ม “Increment” หรือ “Decrement”
dispatch
จะถูกเรียกพร้อมกับ actionincrement()
หรือdecrement()
ตามลำดับ ซึ่งจะส่ง action ไปยัง store และทำให้ค่าcount
ใน state เพิ่มขึ้นหรือลดลง (จาก reducer) - ส่วนที่เกี่ยวกับการใส่ค่าเลขจาก user เมื่อ user ใส่ค่าและคลิกปุ่ม “Set Count” action
setCount(newCount)
จะถูกส่งไปยัง store ด้วยค่าnewCount
ที่ผู้ใช้กำหนด ซึ่งจะ update ค่าcount
ใน state เป็นค่าที่ผู้ใช้กำหนดได้
การใช้ dispatch
ใน Redux คือหัวใจหลักในการเปลี่ยนแปลง state ของ application มันช่วยให้สามารถจัดการการ update state ได้อย่างเป็นระเบียบและมีประสิทธิภาพ โดยทำให้กระบวนการเปลี่ยนแปลง state นั้นสามารถคาดการณ์ได้และง่ายต่อการติดตาม นอกจากนี้ยังช่วยให้สามารถแยกความรับผิดชอบของการอ่าน state (ผ่าน useSelector
) และการ update state (ผ่าน dispatch
) ออกจากกัน ทำให้ code ดู clean มากขึ้นและ maintain ง่ายมากขึ้นเช่นเดียวกัน
นี่คือตัวอย่างภาพรวมของ Redux การส่งข้อมูลจาก view > dispatch > action > reducer > state > view อย่างที่เห็น เมื่อมีการใช้ Redux จะเป็นการ handle state ภายในระบบทางเดียว และจะมีเพียงวิธีเดียวที่สามารถเปลี่ยนข้อมูลใน state ได้ ดังนั้น การ design Redux application เองจะคำนึงถึงการสร้าง action และ reducer (store) เป็นหลักว่าจะใช้ในจุดไหนของ application บ้าง โดยจะ “ไม่ขึ้นอยู่กับ View ที่เรียกใช้” เดี๋ยวเราลองมาดูตัวอย่างที่ลึกซึ้งขึ้นอีกนิดจากตัวอย่างในบทความนี้กัน
มาลองใช้ Redux กัน
ตัวอย่างที่หยิบมาทำกัน จะเป็นการจำลองระบบ user management ขึ้นมา โดย
- มีหน้า list user ทั้งหมดออกมา
- สามารถเข้าไปดูและแก้ไข user แต่ละตัวได้
- ต่อเข้ากับ Mock API (โดยใช้ redux-thunk เป็น middleware สำหรับต่อ API ได้)
เพื่อให้เห็นภาพการใช้งาน action, reducer, store มากขึ้น มาเริ่ม setup project กันโดย
- เราจะใช้ vite สร้าง react ขึ้นมา (เพื่อเริ่มต้น project ตั้งแต่แรกโดยไม่มี template อื่นๆเข้ามาเกี่ยวข้องกัน)
- ใช้ tailwind และ เพิ่ม router เข้ามา (react-router-dom) เพื่อให้สามารถไปมาระหว่างหน้าได้ (ทั้งหมดนี้ ผมเคยแชร์ไว้เรียบร้อยในหัวข้อ React Hook และ Component สามารถไปอ่านเพิ่มเติมได้เช่นกันนะครับ)
เริ่มต้นให้ start project react จาก vite มา
นี่คือโครงสร้างไฟล์ทั้งหมดของ project นี้ (เฉพาะส่วนที่เกี่ยวข้อง)
Setup Redux store - action - reducer
อย่างที่เห็นเรามีการแยก 3 files ออกจากกันคือ
store.js
สำหรับ register store เข้า reduxactions/userActions.js
สำหรับประกาศ action type ของ user เอาไว้reducers/userReducer.js
สำหรับวาง reducer ของ user เอาไว้
เมื่อเรามาดูจากที่ actions/userActions.js
กัน ก็จะเจอ source code หน้าตาประมาณนี้
Code ด้านนี้เป็นตัวอย่างของ action creators ใน Redux ที่ใช้สำหรับจัดการกับการเรียก API เพื่อดำเนินการกับข้อมูลผู้ใช้ (users) โดยมีการใช้ axios
เพื่อทำการเรียก API (ที่เดี๋ยวจะไปใช้ร่วมกับ middleware อย่าง Redux Thunk สำหรับการจัดการกับ asynchronous actions) โดย Action Type ประกอบด้วย
FETCH_USERS
ใช้สำหรับเรียกดูข้อมูลผู้ใช้ทั้งหมดจาก APIFETCH_USER
ใช้สำหรับเรียกดูข้อมูลผู้ใช้ตามuserId
ที่ระบุCREATE_USER
ใช้สำหรับสร้างผู้ใช้ใหม่ใน APIEDIT_USER
ใช้สำหรับแก้ไขข้อมูลผู้ใช้ที่มีอยู่ใน APIDELETE_USER
ใช้สำหรับลบผู้ใช้จาก API
และ Action
- fetchUsers เป็น function ที่ทำการเรียกข้อมูลผู้ใช้ทั้งหมดจาก API แล้วส่งข้อมูลผู้ใช้ที่ได้ไปยัง reducer ผ่าน
dispatch
- fetchUser เป็น function ที่เรียกข้อมูลผู้ใช้เฉพาะตาม
userId
จาก API แล้วส่งข้อมูลนั้นไปยัง reducer - createUser เป็น function ที่สร้างผู้ใช้ใหม่บน API ด้วยข้อมูลผู้ใช้ที่ส่งเข้ามาผ่าน
payload
โดยหาก API ทำส่งข้อมูลสร้างสำเร็จ จะส่งข้อมูลผู้ใช้ใหม่ไปยัง reducer - editUser เป็น function ที่แก้ไขข้อมูลผู้ใช้ที่มีอยู่บน API ด้วยข้อมูลใหม่ส่งเข้ามาผ่าน
payload
- deleteUser เป็น function ที่ลบผู้ใช้จาก API ตาม
userId
ที่ระบุและไม่มีการส่งข้อมูลกลับไปยัง reducer นอกจากuserId
เพื่อยืนยันการลบว่าลบสำเร็จเรียบร้อย
และ ต่อมาที่ reducer ไฟล์ reducers/userReducer.js
จาก code ด้านบนเป็นการประกาศ Reducer สำหรับจัดการข้อมูลผู้ใช้ (users) โดยมีหน้าที่รับ action เพื่อ update state ของ store ตาม logic ที่กำหนดไว้ โดยตัว reducer จะจัดการกับหลาย actions ที่เกี่ยวข้องกับการดึงข้อมูลผู้ใช้, การสร้างผู้ใช้ใหม่, การแก้ไข, และการลบผู้ใช้ (ตาม action type ที่ส่งเข้ามา) โดย
- มี initialState ที่มี
**users**
เป็น array ว่างเปล่า สำหรับเก็บ list ของ user และcurrentUser
สำหรับเก็บข้อมูล user ที่กำลังดูอยู่
และ action กับ state ก็จะมีตามนี้
- FETCH_USERS เมื่อ action นี้ถูก dispatch reducer จะ update state
users
ด้วย list user ที่ได้มาจาก payload (ค่าที่ส่งต่อมาจาก API) - FETCH_USER เมื่อ action นี้ถูก dispatch reducer จะ update
currentUser
ด้วยข้อมูล user ตาม id ที่ได้มาจาก payload (ค่าที่ส่งต่อมาจาก API) - CREATE_USER เมื่อมีการสร้างผู้ใช้ใหม่ state
users
จะถูก update โดยการเพิ่มผู้ใช้ใหม่นั้นเข้าไปในusers
array ของ state - EDIT_USER สำหรับการแก้ไขผู้ใช้ reducer จะทำการค้นหาผู้ใช้ใน array
users
โดยใช้id
และ update ข้อมูลของผู้ใช้นั้นด้วยข้อมูลใหม่จาก payload (ค่าที่ส่งต่อมาจาก API) - DELETE_USER เมื่อต้องการลบผู้ใช้ reducer จะกรอง
users
array เพื่อลบผู้ใช้ที่มีid
ตรงกับที่ส่งมาใน payload (ที่ confirm จาก API มาแล้วว่าลบเรียบร้อย)
และที่ store.js
ทำการ register reducer เข้า store เข้าไป
โดยเราจะนำสิ่งนี้มาช่วยสำหรับการ register store ที่มีการยิง API (asynchronous) นั่นก็คือ Redux Thunk
Redux Thunk เป็น middlewareที่ใช้กับ Redux ซึ่งช่วยให้เราสามารถเขียน action creators ที่ส่งกลับ function แทนที่จะเป็น action objects ได้ ความสามารถนี้ทำให้เราสามารถจัดการกับ action ที่มีลักษณะ asynchronous ได้ เช่นการเรียกข้อมูลจาก API ซึ่งไม่สามารถทำได้โดยใช้ Redux แบบทั่วไปที่สนับสนุนเฉพาะ synchronous actions เท่านั้น (เลยจะแตกต่างกับตัวอย่างก่อนหน้าที่ไม่ต้องใช้ Redux Thunk ก็ได้)
ลองส่งข้อมูลและดึงข้อมูลผ่าน Redux
เมื่อลองมาดูที่ pages/UserList.jsx
โดยในหน้านี้จะเป็นตัวแทนของการดึง list ของ user ออกมา โดยในหน้านี้
- ต้องสามารถดึงข้อมูล user ทั้งหมดออกมาได้
- ต้องสามารถยิงคำสั่งลบ user ตาม id ของแต่ละคนได้
และนี่คือ code ของไฟล์นี้
อย่างที่เห็นจาก code ว่า
- มีการ import action เข้ามาคือ
fetchUsers
,deleteUser
ซึ่งก็คือ action creator (ที่มี action type อยู่ในตัว) ที่ใช้สำหรับการดึง list user และ ลบ user - โดยคำสั่งของ Redux นั้นได้เตรียมคำสั่ง Redux Hook เอาไว้ โดยมี 2 คำสั่งคือ
useSelector
สำหรับการเข้าถึง state จาก Redux store และuseDispatch
สำหรับการ dispatch action (ไปยัง reducer) - เมื่อมีการ render component ขึ้นมา (ใช้ useEffect ดักจับกับ dispatch ไว้จาก
**useDispatch**
) เพื่อทำการ dispatchfetchUsers
action เพื่อทำการ load user list เข้ามาจาก API โดยใน component นี้มีการ access เข้าถึง users array เอาไว้จากuseSelector
- หลังจากที่
**fetchUsers**
เรียบร้อย reducer ก็จะทำการ update state ใน store และตัว**useSelector**
ก็จะรับรู้ว่ามี store เปลี่ยนแปลงค่าอยู่ก็จะทำการเปลี่ยนแปลงค่า state ใน component ผ่าน Hook ของ**useSelector**
ก็จะส่งผลทำให้แสดง list user จาก user store ออกมาได้ - เช่นเดียวกันกับ
**deleteUser**
เมื่อมีการกดลบ user จาก list ไปก็จะมีการ dispatchdeleteUser
พร้อมกับ**user.id**
ส่งไปเพื่อเป็นการบอก**user.id**
ใน action creator ว่าจะต้องส่งไปบอกทั้ง API และ reducer ว่า หาก API เรียบร้อยให้ลบออกจาก store และ เมื่อทำการลบเรียบร้อย ก็จะบอกผ่าน**useSelector
** ตัวเดิมหลังจากที่ update user list เรียบร้อย Component นี้ก็จะได้ user list set ใหม่ออกมาได้นั่นเอง
และที่ pages/UserEdit.jsx
โดยหน้านี้จะต้องทำทั้งหมด 2 โจทย์คือ
- createUser เมื่อเข้ามาหน้านี้แบบไม่มี parameter user id อะไร จะต้องรับข้อมูลจาก form และสร้าง user ใหม่ได้
- editUser เมื่อเข้ามาหน้านี้แบบมี parameter user id จะต้องดึงข้อมูลผ่าน fetchUser และต้องสามารถแก้ไข user คนนั้นตาม user id ที่ส่งมาได้
และนี่คือ code ของไฟล์นี้
อย่างที่เห็นจาก code จะคล้ายๆหน้า UserList คือ
- ตอนเปิดหน้าใหม่มีการดักจับ useEffect ไว้ว่า หากมี id ใน parameter ออกมา ให้ทำการ dispatch ไปยัง fetchUser(id) โดยทำการส่ง id นั้นไป
- เมื่อมีการ ดึงข้อมูลจาก API (ที่ Action Creator) และ update จาก reducer เรียบร้อย ก็จะ update ข้อมูลผ่าน useSelector โดยจะส่งมาผ่าน state currentUser ที่อยู่ใน user store และเมื่อ useEffect ดักจับได้ว่ามี currentUser โผล่เข้ามาก็จะนำข้อมูลนั้นมา set เป็น state ที่ใช้ใน Form Component ใน Component นี้ต่อไปนั่นเอง
- ท้ายที่สุดสำหรับปุ่ม Save ก็มีการ handle เอาไว้ว่า
- ถ้ามี id = dispatch editUser
- ถ้าไม่มี id = dispatch createUser
- และทั้ง 2 การ dispatch ก็จะส่งข้อมูล user เข้าไปเป็น parameter เพื่อไปเป็น payload ในการส่งที่ action creator และนำไปส่ง API และ reducer ต่อไป
และที่ App.jsx
ทำการเรียกใช้ทุกหน้าออกมา
และนี่คือผลลัพธ์ของ code นี้
สังเกตจาก sequence diagram จะเห็นว่า
- เมื่อทำการ design application ด้วย Redux นั้นเส้นทางการเดินข้อมูลจะเหลือเพียงเส้นทางเดียว แทนที่ปกติจะเป็นการ handle state กันไปมา
- ซึ่งด้วยหลักการนี้ ทำให้เราสามารถ design state ของ application ได้ง่ายขึ้นจากการที่เราสามารถคาดเดา state ที่เกิดขึ้นได้ว่าสามารถเกิดขึ้นจาก action อะไรได้บ้าง (จากแต่เดิมที่เราจะเขียนแก้จาก hook กันโดยตรงนั่นเอง)
Redux toolkit
Redux Toolkit (RTK) เป็นชุดเครื่องมืออย่างเป็นทางการจากทีมงาน Redux ที่มีจุดมุ่งหมายเพื่อทำให้การทำงานกับ Redux ง่ายขึ้น มีประสิทธิภาพมากขึ้น และลดความซับซ้อนในการเขียน code ของ Redux ลง โดย RTK ได้รวมเอา practices และ patterns ที่แนะนำในการใช้งาน Redux มาไว้ในตัว เพื่อช่วยให้ developer สามารถจัดการ state ใน application ได้ดียิ่งขึ้น
ทำไม Redux Toolkit ถึงดีกว่า Redux แบบดั้งเดิม
- ลดความซับซ้อนใน code RTK มี
createSlice
ที่ช่วยให้สามารถรวม reducers, action types และ actions ไว้ในที่เดียวกัน ซึ่งช่วยลดจำนวน code และความซับซ้อนในการจัดการกับไฟล์เหล่านี้ได้ให้เหลืองเพียงแค่ไฟล์เดียว - มี Middleware สำหรับการจัดการ Asynchronous Logic RTK มาพร้อมกับ Redux Thunk เป็น middleware โดยตั้งค่าเริ่มต้น ทำให้สามารถจัดการกับ logic ที่เป็น asynchronous ได้ง่ายขึ้นโดยไม่ต้องตั้งค่าเพิ่มเติม
- Simplifies State Mutations ใช้ Immer ภายใน
createSlice
ซึ่งช่วยให้สามารถเขียน logic ที่เปลี่ยนแปลง state ได้ง่ายดาย โดยเขียนเหมือนกับว่ากำลังเปลี่ยนแปลง state โดยตรง แต่จริงๆ แล้วเป็นการทำ immutable update ซึ่งปลอดภัยมากขึ้น - มี
configureStore
ที่ทำให้การตั้งค่า store ของ Redux เป็นเรื่องง่าย โดยมีการตั้งค่า middleware และ reducers ได้โดยอัตโนมัติ - DevTools Extension ที่ปรับปรุงแล้ว โดยมีการตั้งค่า Redux DevTools ทำได้ง่ายขึ้น ด้วยการตั้งค่าอัตโนมัติใน
configureStore
ของ RTK
มาลองเปลี่ยน Redux เป็น Redux Toolkit กัน
สิ่งที่เราทำคือ เราทำการรวม action และ reducer มาใหม่ใน file reducers/userSlice.js
จาก code ด้านบนนี้
- มีการใช้
createSlice
และcreateAsyncThunk
จาก@reduxjs/toolkit
ร่วมกับaxios
เพื่อดึงข้อมูลจาก API และ update state ใน store ของ Redux createAsyncThunk
ใช้สำหรับการดำเนินการแบบ Asynchronous เช่นเคส API (เปรียบเสมือนการใช้ redux thunk โดยตรงได้ทันที) โดย API นั้นก็จะเป็น set เดิมกับที่ใช้ใน Redux แค่เปลี่ยนมาใช้createAsyncThunk
แทน (รวมถึงไม่ต้องมี try, catch ด้วยเช่นกันเพราะ function นี้จัดการให้หมดแล้วเรียบร้อย)createSlice
ใช้สำหรับจัดการ State โดย- กำหนด
initialState
ที่มีusers
,currentUser
,loading
, และerror
เพื่อเก็บข้อมูลผู้ใช้, สถานะการโหลด และ error เอาไว้ extraReducers
ใช้addMatcher
เพื่อจัดการกับสถานะต่างๆ ของ action ที่ส่งมาจากcreateAsyncThunk
โดย- เมื่อ action สิ้นสุดด้วย
/pending
แสดงว่ากำลังโหลดข้อมูล - เมื่อ action สิ้นสุดด้วย
/fulfilled
แสดงว่าดำเนินการสำเร็จ และจะ update ข้อมูลใน state ตามข้อมูลที่ส่งมา - เมื่อ action สิ้นสุดด้วย
/rejected
แสดงว่ามีข้อผิดพลาดเกิดขึ้น - ซึ่งทั้ง 3 อันนี้
createAsyncThunk
เป็นตัวจัดการให้ทั้งหมด และaddMatcher
เป็น function ที่สามารถ group reducer โดยทำการ group ตาม action ที่ต้องการโดยเลือก filter ตาม pending, fulfilled, rejected ตามที่createAsyncThunk
ส่งมา - ก็จะส่งผลทำให้เราสามารถสร้าง
reducer
สำหรับจัดการ loading case และ error case แบบรวมกันได้ และสามารถจัดการ success case แยกกันในแต่ละเคสของ API ออกจากกันออกมาได้
- กำหนด
ถ้าทุกคนลองเทียบกับ code redux ทั่วไปดูจะเจอว่า
- เราไม่จำเป็นต้องประกาศ action type มาประกอบรายตัวอีกต่อไป (ใช้ function เป็นตัวแทนของ action type ไปเลย)
- ไม่จำเป็นต้อง handle API เนื่องจาก error จากจัดการผ่าน reducer ทำให้สามารถ handle error ผ่าน reducer ได้ทันที โดยไม่จำเป็นต้องประกาศ action มาเพิ่มเพื่อจัดการเรื่องนี้ (ถ้าเป็น redux ทั่วไปต้องแยกเคส action type หมดตั้งแต่ loading, error, success)
จาก 2 อย่างนี้เองจึงส่งผลทำให้ code ของ redux ไม่เกิดความซ้ำซ้อน และยังคงได้คุณสมบัติของ redux เช่นเดิมออกมาได้
มาที่ **store.js**
เปลี่ยนมา register store ด้วย userSlice ผ่าน **configureStore**
แทน
ที่เหลือทั้ง **UserList.jsx**
และ **UserEdit.jsx**
ปรับตำแหน่งจากแต่เดิมเรียก action มาเรียกผ่าน **userSlice**
แทน (ใช่ครับ ปรับแค่นี้เลย)
เพียงเท่านี้ก็จะได้ผลลัพธ์เหมือนๆกันออกมาได้ ใน code ที่สั้นลงกว่าเดิม !
เพิ่มเติมการ debug redux
นอกเหนือจากการช่วยงาน development แล้ว ยังมี Plugin ที่สามารถช่วย Debug ได้นั่นคือ Redux Devtools
https://chromewebstore.google.com/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd
Redux DevTools เป็นเครื่องมือที่ช่วยให้ developer สามารถติดตามการเปลี่ยนแปลงใน state ของ web application ที่ใช้ Redux ได้ โดยมันจะช่วยให้เราสามารถดู history ของ actions ที่ถูก dispatch ไปยัง store, สถานะปัจจุบันของ store และแม้กระทั่ง “ย้อนเวลา” กลับไปยังสถานะก่อนหน้า เพื่อทำการ debug ออกมาได้ด้วยเช่นกัน
คุณสมบัติหลักของ Redux DevTools
- Inspection ของ Actions และ State เราสามารถดูรายละเอียดของแต่ละ action ที่ถูก dispatch รวมถึง state ก่อนและหลังการเปลี่ยนแปลงได้
- Time Travel Debugging feature นี้ช่วยให้เราสามารถ “ย้อนเวลา” กลับไปยังจุดใดจุดหนึ่งใน history ของ actions ได้ เพื่อดูว่า web application ของเรามีกำลังทำงานกับ store อย่างไรในขณะนั้นๆ
- การเปลี่ยนแปลง State แบบ Real-time เราสามารถแก้ไข state โดยตรงจาก DevTools และดูผลลัพธ์ที่เกิดขึ้นใน application ของแบบ real-time ได้
ตัวอย่างก็จะเป็นประมาณนี้
หรือสามารถกดดู diff state ที่ละช็อตก็ได้เช่นกัน
สรุป
การใช้ Redux ในการพัฒนา web application นั้นถือเป็นอีกตัวเลือกที่ค่อนข้างยอดนิยมในการพัฒนา web application ด้วย Javascript และ React โดยข้อดีใหญ่ๆของการใช้ Redux ก็คือ
- ความสามารถในการจัดการ State แบบ Centralized
- การทำ Predictable State Updates ที่สามารถใช้ pure functions ในการ update state อย่าง reducer
- การทำงานร่วมกับ DevTools และ Time Travel Debugging ที่ทำให้ debug กับการเปลี่ยนแปลงของ state ได้ง่ายขึ้น
- รวมถึงสามารถใช้งานร่วมกับ Middleware เพื่อเพิ่มความสามารถในการดำเนินการก่อนที่ action จะถึง reducer ได้ซึ่งช่วยอำนวยเคสเพิ่มเติมอย่างการเพิ่ม logic asynchronous หรือ การบันทึก log ได้ด้วยเช่นกัน
จากประสบการณ์ที่เจอมานั้น คนส่วนใหญ่ (รวมถึงตัวผมเอง) เหตุผลใหญ่ๆที่มักเลือกใช้ Redux (ตัว Centralized state management) แทนการใช้ Hook (จัดการ state ผ่าน Component กันเอง) จะมี use case อยู่ประมาณนี้
- web application มีความซับซ้อนสูง ถ้า web application ของคุณมีหลาย components ที่ต้องการเข้าถึงและแก้ไข state ร่วมกัน Redux จะช่วยให้การจัดการ state เป็นไปได้อย่างเป็นระเบียบมากขึ้น
- ต้องการความสามารถในการ debug ที่ดี ด้วย Redux DevTools และความสามารถในการย้อนเวลาของ state ทำให้การ debug web application สามารถทำได้ง่ายขึ้น
- Team development Redux ช่วยให้สามารถกำหนดรูปแบบการทำงานร่วมกันในทีมได้ง่ายขึ้น ด้วย pattern และ practices ที่ชัดเจน
หวังว่าบทความนี้จะเป็นส่วนหนึ่งที่ทำให้คนที่กำลังสนใจ Redux เข้าใจ Redux มากขึ้น และมีโอกาสได้นำ Redux ไปใช้กับ project ในอนาคตกันนะครับ 😁
Github source code
https://github.com/mikelopster/react-redux-example
Reference
- https://react-redux.js.org/
- https://redux.js.org/tutorials/fundamentals/part-5-ui-react
- https://redux.js.org/introduction/why-rtk-is-redux-today
- มาเรียนรู้พื้นฐาน Diagram แต่ละประเภทของงาน development กันมี Video
รู้จักกับ Diagram ที่ใช้บ่อยประเภทต่างๆว่ามีอะไรบ้าง และใช้สำหรับทำอะไรบ้าง
- รู้จักรูปแบบ Authentication ระหว่าง Frontend และ Backendมี Video มี Github
เราจะพามาทำ Authentication กับการ Login กัน ว่ามีกี่วิธีที่สามารถทำได้ และสามารถทำได้ยังไงกันบ้าง ซึ่งจะพาทำกันตั้งแต่ฝั่งของ API Backend
- ลองเล่น Supabase กับ Next.js กันมี Video มี Github
รู้จักกับ Supabase เทคโนโลยีฐานข้อมูลที่เรียกตัวเองว่าเป็น Firebase alternative กันว่าใช้ทำอะไรได้บ้าง
- รู้จักกับ Drizzle ORM ผ่าน Next.jsมี Video
มาทำความรู้จัก Drizzle ORM กัน ว่ามันคืออะไร และทำไมถึงเป็นที่นิยมในวงการนักพัฒนา และลองเล่นกับ Next.js ด้วยกัน