Introduction to Redux: ไม่ได้ยากอย่างที่คิด (หรอ)
Redux เป็น libary ตัวหนึ่งที่ช่วยเราจัดการ states หรือว่าข้อมูลต่างใน application ของเรา แต่ถ้าหากใครเขียน React มาก็จะรู้ว่า เราสามารถจัดการ states ได้ผ่าน useState
hook หรือ this.state
ใน class component อยู่แล้ว ทำไมเราจำเป็นต้องใช้ libary อันอื่นที่ช่วยจัดการข้อมูลในส่วนนี้ด้วย
การเก็บ states ใน component นั้น มีปัญหาอย่างแรกเลยก็คือ states นั้นจะไม่ถูก share ให้กับ component ถึงเราจะสามารถส่ง states ลงไปใน component ลูกได้ผ่าน props แต่ถ้าเราต้องทำการส่ง states ลงไปสัก 3-4 ชั้น ก็จะทำให้ code ของเรารกไม่ใช่น้อย รวมไปถึงการจะแชร์ states ให้หลายๆ component ที่ไม่ได้อยู่ภายใต้ parent เดียวกัน ก็จะทำได้ยากและวุ่นวาย Redux จึงเกิดมาเพื่อแก้ไขปัญหานี้
Redux ทำการเก็บ states ในรูปแบบ centralize หรือรวมศูนย์กลาง ข้อมูลทุกอย่างจะถูกเก็บไว้ที่ Redux ไม่ใช่เป็นการไปผูกกับ component ไหนเลย คิดง่ายๆ ว่ามันเป็นที่เก็บข้อมูลที่อยู่แยกกับ components ต่างๆของเราก็ได้
Redux Lifecycle
สิ่งที่ทำให้หลายๆคนรู้สึกยากกับ Redux ก็คือหลักการทำงานของมันนั้นเอง การจะกำหนด states ใหม่ให้กับ Redux นั้น ไม่สามารถทำได้แบบตรงๆ และการดึงเอา states ออกมาใช้ ก็ไม่ได้ง่ายเช่นเดียวกัน ในบทความนี้ผมจะพูดถึง concept และการใช้งาน Redux แบบง่ายที่สุดก่อน โดยยังไม่นำไปใช้คู่กับ React
การเปลี่ยนแปลง states ที่เก็บไว้ใน Redux จะเริ่มจากการที่ Action
ถูกสร้างขึ้นมา ส่วนใหญ่แล้วจะเกิดจากการที่ผู้ใช้คลิกอะไรสักอย่างใน UI อย่างเช่นกดปุ่ม Login หรือ Logout โดย Action
จะถูกdispatch
หรือส่งต่อไปให้กับ Reducer
ทุกๆตัว โดยตัว Reducer
จะทำหน้าที่เปลี่ยนแปลง states ใน Redux ดังนั้นใน Reducer
จึงมี logic ที่เอาไว้สำหรับประมวลผล Action
ที่เข้ามาและทำการเปลี่ยน states นั้นเอง
Action and Action Creator
Action
คือ JavaScript Object ธรรมดาที่มี key อย่างน้อยหนึ่ง key ซึ่งก็คือ type
เพื่อบ่งบอกว่า Action
อันนี้มี type เป็นอะไร โดยนอกจาก type
แล้ว เรายังสามารถเพิ่ม key อื่นๆ เข้าไปได้ด้วยถ้าหากเราต้องการใช้ข้อมูลนั้น โดยส่วนใหญ่แล้วจะเก็บใน key ชื่อ payload
// This is an action
{
type: "SAMPLE_ACTION" // Can be any string
payload: "Some userful information" // Can by anything
}
// This is also an action
{
type: "ACTION_WITH_NO_PAYLOAD"
}
ถึงแม้ว่าเราจะสามารถสร้าง Action
ขึ้นมาได้จากการสร้าง object ธรรมดา วิธีนี้ไม่ใช่วิธีที่ควรทำนัก เนื่องจากเราอาจจะต้องมีการสร้าง Action
ตัวนี้ในหลายๆ ที่ ดังนั้นเราควรจะสร้างฟังก์ชั่น Action Creator
เพื่อช่วยสร้าง Action
ขึ้นมาใช้แทน เพื่อป้องกับ typo ที่อาจจะเกิดขึ้นได้
const deposit = (amount) => {
return {
type: "DEPOSIT",
payload: amount
}
}
const withdraw = (amount) => {
return {
type: "WITHDRAW",
payload: amount
}
}
const userLogin = (userID) => {
return {
type: "USER_LOGIN",
payload: userID
}
}
const userLogout = () => {
return {
type: "USER_LOGOUT"
}
}
Code ด้านบนนี้เป็นตัวอย่างของฟังก์ชั่น Action Creator ซึ่งท้ายที่สุดแล้วจะ return object ที่มี key type
ออกมา ซึ่งเราเรียกมันว่า Action
โดยในตัวอย่างนี้จะเป็นการเก็บ states ของ userID ในกรณีที่ผู้ใช้เข้าสู่ระบบอยู่ กับ balance ของผู้ใช้ โดยจะมีการทำงาน 2 อย่างก็คือ deposit
และ withdraw
Dispatch
หลังจากการเรียกฟังก์ชั่น Action Creator
และเราได้ Action
ออกมาแล้ว เราจะทำการส่ง Action
ต่อไปภายใน Redux ด้วยการเรียกฟังก์ชั่น dispatch()
ทำให้ Action
ของเราถูกส่งไปยัง Reducer
ต่อไป
Reducer
Action
ทุกตัวที่ถูก dispatch จะถูกส่งเข้ามาที่ Reducer โดย Reducer ก็เป็น function ที่รับ parameter 2 ตัวคือ
- State ปัจจุบัน
- Action ที่ได้รับมา
โดยเมื่อสิ้นสุด function จะต้องทำการ return state ใหม่ออกไป ซึ่งจะเป็นอันเดิมก็ได้ หรือว่าจะเป็นอันใหม่ก็ได้ โดยถ้าเป็นอันใหม่ จะส่งผลให้ component ที่ใข้งาน state นั้นทำการ re-render ใหม่ด้วย
คำว่า state ใหม่นั้นหมายถึงค่า หรือ pointer ค่าใหม่ ถ้าหากเราแค่เพิ่ม key value ลงไปใน object แล้ว return ออกไป pointer ของ object นั้นก็จะไม่ถูกเปลี่ยน ดังนั้น Redux จะมองว่าค่า state นั้นไม่ได้มีการเปลี่ยนแปลง ส่งผลให้ component ที่ใช้งาน state นั้นไม่ทำการ re-render
const balanceReducer = (balance = 0, action) => {
switch (action.type) {
case "DEPOSIT":
return balance += action.payload
case "WITHDRAW":
return balance -= action.payload
default:
return balance
}
}
const authReducer = (userID = null, action) => {
switch (action.type) {
case "USER_LOGIN":
return action.payload
case "USER_LOGOUT":
return null
default:
return userID
}
}
จากตัวอย่าง code ด้านบน จะเห็นได้ว่า Reducer ตัวนี้สนใจเฉพาะ Action
ที่มี type เป็น DEPOSIT, WITHDRAW และ RESET_BALANCE เท่านั้น ถ้าหากเป็น type อื่น ก็จะ return balance ค่าเดิมออกไป ซึ่งก็คือไม่มีการเปลี่ยนแปลงอะไรนั้นเอง
เราควรจะใส่ค่า default ให้กับ parameter ตัวแรก (State ปัจจุบัน) เนื่องจากในการรัน Reducer ครั้งแรก ค่าที่เข้ามาอาจจะเป็น null และจะทำให้เกิด error ได้
ซึ่งในส่วนของ Reducer นี่แหละ ที่จะมี logic ในการจัดการกับ Action
ที่เข้ามายัง Reducer
ให้นึกภาพว่า Action
ที่ถูก dispatch นั้นเป็นเหมือนกับ HTTP Request ที่เข้ามายัง Backend server ของเรา และส่วนของ Reducer
ก็จะทำหน้าที่เหมือนกับ Business Logic ท่ีเราเขียนไว้ใน Backend เพื่อประมวลผล request ที่เข้ามา
States initialization
เมื่อ Redux เริ่มทำงาน เราต้องประกาศว่าเราต้องการใช้ Reducer อะไรบ้างใน application ของเรา โดยสามารถทำได้จากฟังก์ชั่น combineReducers()
โดยจะต้องใส่ object เป็น parameter ที่มี key คือชื่อ state กับ value เป็น reducer ที่เราต้องการ
const reducers = combineReducers({
balance: balanceReducer,
userID: authReducer
})
หลักจากเรารวม Reducer ทั้งหมดของเราเข้าด้วยกัน ขั้นตอนสุดท้ายคือการสร้าง store สำหรับเก็บ state ของเราจาก reducer ที่เราเพิ่งรวมไปเมื่อกี้นี้ผ่านฟังก์ชั่น createStore()
const store = createStore(reducers)
เพียงเท่านี้ Redux ก็พร้อมที่จะทำงานแล้ว
Let's try it
เมื่อมีเหตุการณ์ให้เราต้องการเปลี่ยน states ที่เก็บเอาไว้ใน Redux สิ่งแรกที่เราต้องทำก็คือการสร้าง Action
ที่เราต้องการขึ้นมาก่อน ยกตัวอย่างเช่นถ้าหากผู้ใช้ทำการฝากเงิน 500 บาท ผมก็จะสามารถสร้าง Action
จาก Action Creator
และทำการ dispatch ออกไป
const depositAction = deposit(500)
store.dispatch(depositAction)
หรือเราก็สามารถรวบมาอยู่ใน statement เดียวได้ดังนี้
store.dispatch(deposit(500))
เมื่อ Action
ถูก dispatch แล้ว Action
ก็จะถูกส่งไปให้กับ Reducer ทุกตัว ถ้าหาก Reducer ตัวไหนสนใจใน Action นั้นก็สามารถประมวลผล ตามที่ code ได้เขียวไว้และ return states ใหม่ออกมาได้
เราสามารถดู states ใน Redux ได้ผ่าน store.getState()
ซึ่งหลังจากการทำการฝากเงินไป Redux ของเราก็จะมี State เป็น
{
balance: 500,
userID: null
}
Note: ถ้าหากเราเรียกstore.getState()
ก่อนที่จะ dispatchAction
เราก็จะได้ค่า Default value ที่เราใส่เอาไว้ใน parameter ตัวแรกของ Reducer ที่เราเขียนเอาไว้ ในกรณีนี้ก็คือ 0 กับ null
ถ้าหากเราลองเปลี่ยนแปลง states หลายๆ รอบตามนี้
store.dispatch(userLogin(5))
store.dispatch(deposit(100))
store.dispatch(withdraw(50))
store.dispatch(userLogout())
store.dispatch(withdraw(250))
store.dispatch(userLogin(8))
ผลลัพท์สุดท้ายก็จะได้เป็น
{
balance: 300,
userID: 8
}
เพียงเท่านี้เราก็สามารถจะเปลี่ยนแปลง States ใน Redux ได้แล้ว
The Hardest Part
สิ่งที่ยากในการใช้ Redux ไม่ใช่เราเข้าใจ concept และการทำงานของมัน (ซึ่งจริงๆ ก็เข้าใจยากแหละ) แต่คือการนำเอาไปปรับใช้ การออกแบบว่า Application ของเราจะต้องมี Action
และ Reducer
อะไรบ้างในการทำงานนี่แหละ เป็นสิ่งที่ผมว่ายากที่สุดของ Redux ส่วนหนึ่งก็เป็นเพราะ Process ของมันที่ไม่ค่อยตรงกับ libary ที่ใช้จัดการ states อื่นๆ สักเท่าไหร
Code ตัวอย่างที่ผมเขียนสามารถดูจากได้ที่ https://aka.cscms.me/basic-redux-demo