Redux เป็น Library ที่จะช่วยควบคุมการไหลของข้อมูลใน Application ของเราให้ดีขึ้น ซึ่งจะช่วยให้เราคาดการได้ว่าเมื่อเกิดเหตุการณ์ใดใน Component แล้วจะมีผลอะไรกับ Application ของเราบ้าง ถ้ายังนึกภาพไม่ออกให้ดูรูปนี้
จากรูป State จะถูก Set และ เรียกใช้ภายใน Component ของใครของมัน โดยส่งข้อมูลหากันระหว่าง Component ด้วย Props ทําให้เมื่อ Application ของเรามีขนาดใหญ่ขึ้นจะเกิดปัญหาเหล่านี้
- การใช้สถานะร่วมกันระหว่างคอมโพแนนท์ ถ้าใน Appliction ของเรามี Component สอง Component ที่ใช้สถานะร่วมกัน ปัญหาคือเราจะเก็บ สถานะเหล่านั้นไว้ที่ Component ไหนดี หรือเอาไว้ทั้งสอง Component เลย ก็จะทําให้เกิดปัญหาตามหัวข้อถัดไปอีก คือ โค๊ดที่ซํ้าซ้อน
- **โค๊ดที่ซ้ำซ้อน **มีหลายคอมโพแนนท์ที่มีสถานะ (state) ร่วมกัน เช่นถ้าเรามี Component A สําหรับกรอกข้อมูล และเมื่อกด Submit แล้วก็ให้ Component B ให้การแสดงข้อมูลที่กรอกไปเมื่อตะกี้ จะดีกว่ามั้ยถ้าเราจะนําข้อมูลจาก Component A มาแสดงผลได้เลย โดยไม่ต้องจัดเก็บสถานะไว้ที่ Component ตัวเอง ซึ่งจะทําให้เราสับสนได้
- เส้นทางของข้อมูลไม่ได้ไปในทิศทางเดียวกัน จากรูปด้านบนจะเห็นว่าในแต่ละ Component จะมี สถานะ (state) ของใครของมัน และข้อมูลวิ่งเข้าออกได้อย่างอิสระ จะทําให้เส้นทางของข้อมูลถูกส่งกันระหว่าง Component ไม่เป็นไปในทิศทางเดียว เมื่อระบบใหญ่ขึ้นเราจะคาดการณ์ได้ลําบากเมื่อเกิด Action ใน Component ใด Component หนึ่ง
เมื่อเราทราบปัญหาแล้ว สิ่งที่จะมาช่วยจัดการกับ State ของเราก็คือ Redux นั่นเอง
หลักการของ Redux
Redux จะช่วยจัดการ State ทั้งหมดของ Appliction เรา ให้เส้นทางของข้อมูลไหลไปในทิศทางเดียว ตามรูปได้ด้านล่างจะเปรียบเทียบถ้ากรณีที่เราไม่ได้ใช้ Redux (รูปด้านซ้ายมือ) เมื่อเกิด State change (วงกลมสีม่วง) ข้อมูลจะถูกส่งไปยัง Component ต่างๆ อย่างไปเป็นระบบ ทําให้เราคาดการผลที่จะเกิดขึ้นกับระบบได้ยาก แต่ถ้าเราใช้ Redux (รูปด้านขวา) ซึ่งจะแยก State ออกไปเก็บไว้ที่เดียว ซึ่งทุกๆ Conponent จะมาเรียกเอาข้อมูลไปใช้เมื่อต้องการ และการเปลี่ยน State (เส้นสีเขียว) Component จะไม่สามารถเปลี่ยนได้โดยตรง จะต้องเปลี่ยนผ่าน Action เท่านั้น (เดี๋ยวจะลงรายละเอียดอีกที)
Redux มีหลักการอยู่ 3 ข้อ ดังนี้
1. Single Source of Truth ความจริงมีเพียงหนึ่งเดียวเท่านั้นที่ Store
React จะสร้างสิ่งหนึ่งขึ้นมา เรียกว่า Store ซึ่งจะเป็นที่สําหรับเก็บ State ทั้งหมดของ Appliction ไว้ ทุกๆ Component จะมาเรียกข้อมูล State เพียงที่เดียวเท่านั้น เมื่อมีเพียงที่เดียวทําให้เวลาเรา Debug ดูข้อมูลจะทําได้ง่ายขึ้น ก็แค่มาดูที่ Store ที่เดียว
2. State เป็น Read-only
State ที่อยู่ใน Store จะยอมให้อ่านอย่างเดียวห้ามแก้ เพราะว่าถ้าถูกแก้ไขได้โดยตรงจาก Component เราจะ track ได้ยากมากว่าใครมันมาแก้ และอาจจะถูกแก้แบบมักงายได้ แล้วจะแก้อย่างไรละ การจะแก้ไข State ที่อยู่ใน Store จะต้องแก้ผ่าน Action เท่านั้น ซึ่ง Action จะมีหน้าที่บอกกับ Store ว่า เกิด Action อะไรขึ้น และมีข้อมูลอะไรแนบมามั้ย ตัวอย่างเช่น ถ้าเราต้องการจะแก้ไขค่า counter ที่อยู่ใน Store เราก็ต้องสร้าง Action ประมาณนี้ส่งไป ซึ้งเป็นข้อมูล Object ธรรมดา
{
type: 'SET_COUNTER',
payload: 100
}
- type คือตัวที่บอกว่าเป็น Action อะไร
- payload จะเป็นข้อมูลที่แนบไปด้วย ในที่นี้หมายถึงต้องการเปลี่ยน counter เป็น 100
3. การเปลี่ยน State ต้องเป็น Pure function เท่านั้น โดยทำผ่าน Reducers
เมื่อสร้าง Action เราจะใช้ Dispatch ในการนําส่ง Action มาให้ Reducer ทีนี้คนที่จะเปลี่ยน State ได้ จะต้องทำผ่าน Pure function เท่านั้น โดยคนที่จะทำการแก้ State นั้นคือ Reducers ครับ Reducers จะเป็น function ที่มีหน้าที่ในการดูว่าตอนนี้ state เป็นอะไร และถ้า Action นี้มา State ใหม่จะเป็นอะไร ตัวอย่าง Code
function visibilityFilter(state = 0, action) {
switch (action.type) {
case 'SET_COUNTER':
return action.payload
default:
return state
}
}
จาก Code ด้านบน State เดิมเป็น 0 และถ้า action.type เป็น SET_COUNTER จะ Return state ใหม่ออกไป ซึ่ง state ใหม่ก็คือ payload ของ action ที่เราส่งแนบมาด้วย ข้อมูลของ State ใหม่จะถูกเก็บลง Store
ถ้า action.type ไม่ตรงกับ case ไหนเลย จะ Return state เดิมกลับไป
สิ่งที่ต้องระวังคือ เราไม่ควรแก้ไขข้อมูล State เดิมของระบบ แต่ให้เรากําหนดข้อมูลใหม่เสมอ ผมจะยกตัวอย่างการแก้ข้อมูลของ State เก่าตาม Code ด้านล่าง ซึ่งไม่ควรทําอย่างยิ่ง
function visibilityFilter(state = 0, action) {
switch (action.type) {
case 'SET_COUNTER':
state.counter = action.payload
return state
default:
return state
}
}
ภาพรวมของ Redux จะเป็นไปตามรูปด้านล่างครับ
เมื่อ page ถูกโหลดขึ้นมา จะมี Action แรก (Action ด้านซ้ายมือ) จะส่ง Action ไปให้ Reducer ผ่านทาง Dispatch แล้ว Reducer จะ Initial state เริ่มต้น เพื่อเก็บลง Store เมื่อ Store เก็บ State แล้วจะส่งไปบอก UI หรือ Container ที่ Connect หรือ Subscribe อยู่ ให้ปรับเปลี่ยน UI ตาม State ที่ถูกเปลี่ยนเเปลงไป หลังจากนี้ถ้ามีการคลิกปุ่ม หรือ เกิด Action ใดๆ ขึ้นบน UI หรือ Container ตามที่เรากําหนดไว้ ตัว Container จะส่ง Event ไปสร้าง Action ขึ้นมา แล้วส่งให้ Reducer ผ่านทาง Dispatch แล้ว Reducer จะทําการแก้ไข State ที่อยู่ใน store ตามที่ Action บอก เมื่อ State ใน Store เปลี่ยน Store จะส่งไปบอก UI หรือ Container เพื่อปรับเปลี่ยนการแสดงผลให้เข้ากับ State ที่เปลี่ยนไป (ดูรูปภาพด้านบนประกอบ)
Container ก็คือ UI หรือ Component
Presentational component กับ Containers components
มาถึงตรงนี้อยากให้คำนึงว่า React จะสนใจแค่ View (Container ต่างๆ) แต่ Redux จะสนใจการจัดการข้อมูลของ state ผ่าน store นะครับ ดังนั้นเราจะมาดูแนวคิดของการจัดการ Container ต่างๆ โดยจะแบ่ง Component ออกเป็นสองประเภทคือ
- Container Component นั้นฉลาดและรู้ว่าเมื่อมีเหตุการณ์อย่างหนึ่งอย่างใดเกิดขึ้น ควรจะตอบสนองต่อเหตุการณ์นั้นอย่างไร
- Presentational Component นั้นเป็นเพียงคอมโพแนนท์ที่แสดงผลอย่างเดียว
วิธีการจัดการเหตุการณ์จะกำหนดไว้ใน Container Component เมื่อมีเหตุการณ์อะไรก็ตามเกิดขึ้น Presentational Component ที่ถือเป็น subcomponent หรือคอมโพแนนท์ลูกของ Container จะโยนเหตุการณ์นั้นขึ้นไปเรื่อยๆ จนกระทั่งถึง Container Component ที่อยู่บนสุด เพื่อให้คอมโพแนนท์ที่ชาญฉลาดตัวนี้จัดการกับเหตุกาณ์นั้น มีเพียง Container Component เท่านั้นที่รู้ว่าจะจัดการเหตุการณ์อย่างไร
ตัวอย่างการใช้งาน Redux
ผมจะลองสร้างเว็บ Counter ขึ้นมา โดยมีหน้าจอแบบนี้
เมื่อกดปุ่ม Counter จะเพิ่มขึ้นครั้งละ 1 โดยผมจะออกแบบ Web Application ตามนี้
ผมจะมี Component index เป็น Component หลัก และภายในจะมี Component App ซึ่งภายใน Component App จะมี Component ViewCounter (สําหรับแสดงผล Counter) กับ Component increaseButton (ปุ่ม Increase Counter) โดยการทํางานจะเป็นแบบนี้ครับ ให้ดูภาพด้านบนประกอบ เมื่อคลิกปุ่ม Increase Counter เจ้า Component increaseButton จะส่ง event ไปให้ Component App หลังจากนั้น Component App จะส่ง Event เพื่อไปสร้าง Action ที่ Action Creator แล้วส่ง Action ไปเพื่อเปลี่ยน State (เพิ่มค่า counter)ใน Store เมื่อ State ใน store เปลี่ยน Component viewCounter ที่ subscribe store ไว้ จะเปลี่ยนแปลงการแสดงผลตาม State ที่เปลี่ยนไป
ลงมือ Coding
เราจะสร้างไฟล์มาแบบนี้ครับ
ผมจะแยก Folder ไว้ตามนี้ครับ
- actions เป็นที่เก็บ action ทั้งหมดของ Application
- components เป็นที่เก็บไฟล์ presentational component
- containers เป็นที่เก็บ container component
- reducers เป็นที่เก็บไฟล์ reducer
- store เป็นที่เก็บไฟล์ store
ไฟล์แรกที่เราจะเขียนคือ “./store/store.jsx เป็นไฟล์ที่ใช้สร้าง Store
import { createStore } from 'redux'
import rootReducer from '../reducers'
export default () => {
return createStore(rootReducer)
}
ต่อมาเราจะสร้าง reducer เพื่อใช้เพิ่ม Counter โดยสร้างไฟล์ “./reducers/index.jsx” ก่อน ซึ่งจะเป็นตัวรวม ในกรณีที่มีหลาย Reducer
import { combineReducers } from 'redux'
import counter from './counter.jsx'
export default combineReducers({
counter
})
สังเกต เราจะ import counter.jsx เข้ามา ซึ่ง counter.jsx จะเป็น Reducer ที่จัดการในส่วนของ การเพิ่ม Counter สร้างไฟล์ “./reducers/counter.jsx”
const initialState = 0
export default (state = initialState, action) => {
switch (action.type) {
case "INCREASE_COUNTER":
return state + action.payload
default:
return state
}
}
จาก code เราจะกําหนดให้ ค่าเริ่มต้นเป็น 0 ก่อน และให้ Action type เป็น INCREASE_COUNTER ต่อไปมาดูในส่วนของ Action ที่ไฟล์ “./actions/counter.jsx”
export const increase = () => ({
type: 'INCREASE_COUNTER',
payload: 1
})
จาก code จะสร้าง Action ที่มี Action type เป็น INCREASE_COUNTER เพื่อไปสั่ง ให้ Reducer ทําการเพิ่ม Counter ใน State มาดูที่ไฟล์ “./containers/counter.jsx”
import React, { Component } from 'react'
import { Provider, connect } from 'react-redux'
import store from '../store'
import ViewCounter from '../components/ViewCounter.jsx'
import IncreaseButton from '../components/IncreaseButton.jsx'
import { increase } from '../actions/counter.jsx'
const mapDispatchToProps = dispatch => {
return {
increaseNumber: () => dispatch(increase())
}
}
export default connect(
null,
mapDispatchToProps
)(class extends Component {
render() {
console.log(this.props)
return <div>
<ViewCounter />
<IncreaseButton buttonClick={this.props.increaseNumber} />
</div>
}
})
component นี้จะต้อง subscribe หรือ connect ไปยัง Store แล้วเราจะ import “../actions/counter.jsx” เข้ามาเพื่อใช้สร้าง Action ก่อนที่จะส่งให้ Reducer ด้วยการเรียก function dispatch ตาม Code ด้านบน ไฟล์ “./components/ViewCounter.jsx”
import React from 'react'
import { connect } from 'react-redux'
const ViewCounter = (props) => {
return <div>
Counter: {props.counter}
</div>
}
export default connect(
state => ({ counter: state.counter })
)(ViewCounter)
Component นี้จะต้อง connect ไปเช่นกันเพราะต้องเอาข้อมูล counter จาก Store มาแสดงผล ไฟล์ “./components/IncreaseButton.jsx”
import React from 'react'
export default (props) => {
return <button onClick={props.buttonClick}>Increase Counter</button>
}
Component นี้มีหน้าที่อย่างเดียวคือส่ง event ออกไปให้ Component App เมื่อมีคนคลิกปุ่ม Code เต็มๆจะอยู่บน Github ครับ สังเกตุว่าทุก Component จะไม่รับรู้เลยว่าเมื่อเกิด Event ขึ้น แล้วจะทํา Process อะไร มันรู้เพียงแค่ว่าจะต้องโยน Event ไปให้ใครสักคนจัดการต่อ ซึ่งคนคนนั้นก็คือ Action creator นั่นเอง
ที่มาของข้อมูลและรูปภาพ http://image.slidesharecdn.com/react-slides-140706092503-phpapp02/95/reactjs-or-why-dom-finally-makes-sense-11-638.jpg?cb=1404638753 https://css-tricks.com/uploads/2016/03/redux-article-3-03.svg