စာဖတ်သူအနေနဲ့ Front-end development ၊ (Web ဖြစ်ဖြစ် Mobile ဖြစ်ဖြစ်) ဘယ် User Interface Development တိုင်းမှာ အမြဲတမ်းလိုလို တိုက်ရတဲ့ Monster တစ်ကောင်ကို ပြောပါဆိုရင် ဘယ်ဟာဖြစ်မလဲ။ ကျနော်ကတော့ FE မှာ State Management လို့ခေါ်တဲ့ Application ရဲ့ ဘယ်အချိန်မှာ ဘယ်ဟာကိုပြရမယ် ၊ ဘာလုပ်ရင် ဘာဖြစ်ရမယ်ဆိုတဲ့ အရာကို ကိုင်တွယ်ရတာ အခက်ခဲဆုံးလို့ ထင်ပါတယ်။
ကျားကြီးလေ ခြေရာကြီးလေ ဆိုသလို Application ရဲ့ Scope ကြီးလာတာနဲ့အမျှ ထိန်းသိမ်းရတဲ့ Application State ကလည်း ကြီးလာတာ သဘာဝပါပဲ။ ဆိုတော့ ဒီလို ပြဿနာကို Front-end application တွေမှာ ဘယ်လိုဖြေရှင်းလဲပေါ့။ ဒီဆောင်းပါးမှာတော့ ကျနော်တို့ React
app တစ်ခုမှာ State management ကို ဖြေရှင်းပေးတဲ့ Solution တချို့ကို ချဥ်းကပ်ကြည့်ကြပါမယ်။
1. useState
အားလုံးသိကြတဲ့အတိုင်း React ရဲ့ default state management solution ကတော့ useState hook
ပါပဲ။
// most basic counter ever
function Counter() {
const [count, setCount] = useState(0)
const increment = () => {
setCount(prevCount => prevCount + 1)
}
return (
<div>
<h3>Clicked {count} times</h3>
<button onClick={increment}>Increment</button>
<div>
)
}
ဒီ hook ကိုတော့ Component level လောက်မှာပဲ သုံးဖို့သင့်ပါတယ်။ အမြဲတမ်း Component တစ်ခုအတွင်းမှာပဲ သုံးရမယ်တော့ ဆိုလိုတာမဟုတ်ပါဘူး။ သာမန် React application တစ်ခုမှာ data ဟာ top down သွားတာဖြစ်တဲ့အတွက် Child component
တွေဆီကို props
အနေနဲ့ pass ပေးလို့ရပါတယ်။ အဓိကကတော့ ကိုယ် ဘယ်လောက်နက်နက်ထိ pass မှာလဲ ဆိုတာက အရေးကြီးတာပါ။ ကျနော်ကတော့ depth ကို 1 or တစ်ခါတလေ 2 လို့ အများဆုံးလို့ ယူဆပါတယ်။ ဒီထက်နက်နက်ကို pass ပေးချင်တဲ့ဆိုရင်တော့ တခြား solution သုံးဖို့သင့်ပါပြီ။
2. useReducer
useState
လိုမျိုး နောက် hook တစ်ခုဖြစ်တဲ့ useReducer
ကတော့ နည်းနည်းပိုရှုပ်ထွေးတဲ့ state update logic တွေမှာ သုံးတာပိုသင့်လျော်ပါတယ်။ အဓိက ကတော့ update လုပ်တဲ့နေရာမှာ state တစ်ခုနဲ့တစ်ခု မှီခိုနေတဲ့အချိန်မျိုးမှာပါ။
Reducer
ဆိုတာကတော့ အရှင်းဆုံးပြောရရင် လက်ရှိstate
နဲ့ လုပ်ဆောင်မယ့်action
ကိုလက်ခံပြီးupdated state
ကို ပြန်ပေးတဲ့ သာမန်function
ပဲဖြစ်ပါတယ်။ (Redux နဲ့ ရင်းနှီးတဲ့သူတွေတော့ နားလည်ပြီးဖြစ်မှာပါ။)
const initialState = {
count: 0,
};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
(useState
နဲ့ useReducer
ကို သုံးတဲ့နေရာမှာ ပိုပြီးကွဲပြားသွားအောင်တော့ နောက်ဆောင်းပါးတစ်ခုမှာ ထပ်ပြီးဆွေးနွေးသွားပါမယ်။)
3. React Context
Context
ကတော့ React 16.3 လောက်မှာ စပြီးပါလာတဲ့ API ဖြစ်ပါတယ်။ အပေါ်မှာပြောခဲ့အတိုင်း component layer တွေများလာတဲ့အခါမှာ data ကို props
အနေနဲ့ပဲ pass ပေးနေလို့ အဆင်မပြေပါဘူး။ ဒါကြောင့် Application ရဲ့ Data အတူတူ ဥပမာ - theme varaible လိုမျိုးကို share သုံးကြတဲ့ Component tree တွေမှာ Context API
ကို သုံးတာက ပိုသင့်လျော်ပါတယ်။
export const ThemeContext = React.createContext('light')
function App() {
return (
<ThemeContext.Provider value="light">
{/* children ... */}
</ThemeContext.Provider>
)
}
...
...
...
// nest layer depth တစ်ခုခုမှာ ပြန်သုံးတဲ့အခါ
function MyButton() {
const theme = React.useContext(ThemeContext)
// value will be either light or dark
return <StyledButton theme={theme.value} />
}
4. Context + useReducer
ကျနော်တို့ React ကို စလေ့လာတည်းက အမြဲတမ်းလိုလို တွဲတွေ့ရမှာကတော့ Redux
ဆိုတာပါပဲ။ Redux
က state management library တစ်ခုဖြစ်ပါတယ်။ Redux ရဲ့ philosophy က တော်တော်လေး ကောင်းပေမယ့် ကျနော်တို့ ဒီအတွက်ကို library သပ်သပ် install လုပ်နေရမှာပါ။ သာမန် Application လောက်မှာ Library မလိုအပ်ပဲ သုံးနေရင် application size ဖြစ်သင့်တာထက် ကြီးနေတော့တာပေါ့။ ဒီအတွက်ကို ကျနော်တို့ Context API
နဲ့ useReducer
ကို ပေါင်းသုံးပြီး Redux ရဲ့ shared store ပုံစံမျိုး တုလို့ရပါတယ်။
/*
* A simple store like in Redux
*/
export const MyCtx = createContext()
export const MyProvider = ({ reducer, initialState, children }) => {
return (
<MyCtx.Provider value={useReducer(reducer, initialState)}>
{children}
</MyCtx.Provider>
)
}
// a hook for accessing your context
export const useCtxValue = () => useContext(MyCtx)
// -----------------------------
// then, you can use this as a store in any place you'd like
// your state
const initialState = {
// ...
}
// your reducer
const reducer = (state, action) => {
switch(action.type) {
// ...
default:
return { ...state }
}
}
function ParentComponent() {
return (
<MyProvider initialState={initialState} reducer={reducer}>
{/* your child components that will consume */}
</MyProvider>
)
}
function ChildComponent() {
const [state, dispatch] = useCtxValue()
return (
<div>
{/* show state values or dispatch actions to update state */}
</div>
)
}
အပေါ်က configuration မျိုးဟာ ရိုးရှင်းပြီးတော့ React
ရဲ့ Core API
တွေကိုပဲ သုံးတာဖြစ်တဲ့အတွက် extra dependency လည်း မလိုအပ်ပါဘူး။ Redux သုံးနေကြသူဆို သိပါလိမ့်မယ်။ Redux ကို setup လုပ်ရတာမှာ boilerplate code တွေ တခြား dependency တွေ setup လုပ်ရတာနဲ့တင် တော်တော်အချိန်ကုန်ပါတယ်။ (အခုတော့ Redux toolkit ရှိပါတယ်။) နောက်ပြီး small to medium size Application တော်တော်များများမှာ သပ်သပ် State Management library မလိုပဲ ဒီလို setup နဲ့တင်သွားလို့ရပါတယ်။
ပြောစရာရှိတာကတော့ Scalability ပေါ့။ Application size ကြီးလာတာနဲ့ Context တွေကို ခွဲပြီးထားရတာ များလာမှာတော့ အမှန်ပါပဲ။ ဒါပေမယ့် Context များလာတာက အဓိက ပြဿနာတော့ မဟုတ်ပါဘူး။ တချို့ Context Provider ကို ထောင်နဲ့ချီပြီးတောင် ထားတာမျိုးတွေ ရှိပါတယ်။ အဓိက ပြဿနာ ဖြစ်တာကတော့ Context Provider ဘယ်နှစ်ခုထားရမယ်မှန်း မသိနိုင်တဲ့ ကိစ္စမျိုးတွေမှာပါ။ ဘယ်လိုလဲဆိုတော့ ကိုယ့်ရဲ့ Component က User create လုပ်မယ့်ဟာမျိုးဖြစ်လို့ dynamic ဖြစ်နေမှာဖြစ်တဲ့အတွက် Context provider ကလည်း Dynamic ဖြစ်နေတော့မယ့် အချိန်မျိုးမှာပါ။ ဒီလိုအခြေအနေမျိုးမှာတော့ Context + useProvider
က မသင့်လျော်တော့ပြန်ပါဘူး။
5. Redux
Redux ကတော့ third-party library တစ်ခုဖြစ်တဲ့အပြင် လက်ရှိပြောခဲ့သမျှထဲမှာ Ecosystem rich အဖြစ်ဆုံးလည်းဖြစ်ပါတယ်။ သူ့မှာ Middleware
လို့ခေါ်တဲ့ တခြား plugin တွေ ထည့်သုံးလို့ရပါတယ်။ ဥပမာ redux store ရဲ့အခြေအနေကို တောက်လျှောက် log ထုတ်ပေးနေမယ့် middlewareredux-logger
လိုမျိုးပေါ့။
Redux ဟာ global store ဖြစ်တဲ့အတွက် တစ်နေရာထဲမှာ state ကို သိမ်းထားတာဖြစ်ပါတယ်။ ဒီသိမ်းထားတဲ့ data တွေကို application ရဲ့ ဘယ်လောက်နက်တဲ့ ဘယ် layer ကနေမဆို လှမ်းယူပြီးသုံးနိုင်ပါတယ်။ ဒါ့ကြောင့် Application တစ်ခုလုံး share သုံးမယ့် state မျိုးတွေမှာ Redux က တော်တော်အဆင်ပြေပါတယ်။
ဒါပေမယ့် အများစု Redux ကို သုံးတဲ့ပုံစံကတော့ API ကနေ fetched ပြီး ရလာတဲ့ data တွေကို သိမ်းထားတာမျိုးဖြစ်ပါတယ်။ အမှန်တော့ ဒီလို API fetched ပြီး ရလာတဲ့ data ကို သိမ်းဖို့အတွက် Redux သပ်သပ်သုံးဖို့ မလိုပါဘူး။ Redux မှာ data fetching လုပ်ရတာ တော်တော်လေး verbose ဖြစ်တဲ့အတွက်ကြောင့်ပါ။ ဥပမာ - fetch operation တစ်ခုအတွက်ကို
- action သုံးခု (fetching, success, failure)
- reducer သုံးခု (fetching, success, failure)
တွေရေးရမယ့်အပြင် side effect အတွက် middleware (redux-saga သို့ redux-thunk) ကို dependency အနေနဲ့ ထည့်ရပါဦးမယ်။
အခုနေ React beginner တစ်ယောက်က Redux ကို လေ့လာရမလားလို့ ကျနော့်ကို လာမေးခဲ့ရင်တော့ မလုပ်ပါနဲ့လို့ တားမိမှာပါ။
ဒါဆို data fetching မှာ Redux အစား management အတွက် ဘာတွေသုံးနိုင်သလဲပေါ့။
6. Data fetching libraries
သာမန် Fetched ပြီး သိမ်းမယ့် state မျိုးတွေအတွက် data fetching library တွေက ပိုသင့်လျော်ပါတယ်။ ကိုယ် consume လုပ်ရမယ့် API အမျိုးအစားအပေါ် မူတည်ပြီး GraphQL ဆိုရင် Apollo ၊ Relay ၊ REST API ဆိုရင် react-query ၊ swr တို့ကို သုံးလို့ရပါတယ်။ ဒီ library တွေမှာ caching mechanism လိုကောင်မျိုးတွေ ပါပြီးသားဖြစ်တဲ့အပြင် များသောအားဖြင့် zero configuration ပဲဖြစ်ပါတယ်။
7. Recoil
အခုနောက်ပိုင်းမှာ နာမည်ကြီးနေတာကတော့ Recoil ဖြစ်ပါတယ်။ Recoil က data flow graph
လို့ခေါ်တဲ့ graph တစ်ခုပေါ်မှာ မူတည်ထားတာဖြစ်ပါတယ်။ သူ့မှာ Redux တုန်းကလို အကုန်လုံးက ဝိုင်းသုံးတဲ့ Global store တစ်ခုတည်း မရှိပဲနဲ့ Atom
လို့ ခေါ်တဲ့ shared units of state
တွေနဲ့ အလုပ်လုပ်ပါတယ်။ ဒီ Atom
တွေကို multiple component တွေကနေ subscribe
လုပ်နိုင်ပြီး state update
ဖြစ်တဲ့အခါတိုင်းမှာ subscribed component တွေကလည်း rerender
ဖြစ်မှာပါ။
// taken from recoil docs
const fontSizeState = atom({
key: 'fontSizeState',
default: 14,
});
// ----- subscribing from a component ------
function FontButton() {
const [fontSize, setFontSize] = useRecoilState(fontSizeState);
return (
<button onClick={() => setFontSize((size) => size + 1)} style={{fontSize}}>
Click to Enlarge
</button>
);
}
// ----- another component ------
function Text() {
const [fontSize, setFontSize] = useRecoilState(fontSizeState);
return <p style={{fontSize}}>This text will increase in size too.</p>;
}
ဒီ library ကလည်း manage လုပ်စရာ state တွေအများကြီးရှိတဲ့ application မျိုးအတွက် design လုပ်ထားတာဖြစ်တဲ့အတွက် သာမန် application မျိုးတွေအတွက် overkill ဖြစ်နိုင်ပါတယ်။
8. State Machines
နောက်ဆုံးတစ်ခုကတော့ State Machine တွေပါ။ ဒီမှာတော့ XState
လို့ခေါ်တဲ့ library ကိုသုံးနိုင်ပါတယ်။ React အတွက်လည်း hook တွေရှိပါတယ်။ Redux လိုပဲ State machine တွေရဲ့ state update flow လုပ်ပုံဟာ dispatch လုပ်လိုက်တဲ့ event ကို လက်ရှိ state နဲ့ evaluate လုပ်ပြီး နောက်အသစ်ဖြစ်မယ့် state ကို ပေးတာပဲဖြစ်ပါတယ်။
current state + action event ⇒ next state
// taken from https://xstate.js.org/docs/packages/xstate-react/
import { useMachine } from '@xstate/react';
import { createMachine } from 'xstate';
export const toggleMachine = createMachine({
id: 'toggle',
initial: 'inactive',
states: {
inactive: {
on: { TOGGLE: 'active' }
},
active: {
on: { TOGGLE: 'inactive' }
}
}
});
// in the Toggler component
export const Toggler = () => {
const [state, send] = useMachine(toggleMachine);
return (
<button onClick={() => send('TOGGLE')}>
{state.value === 'inactive'
? 'Click to activate'
: 'Active! Click to deactivate'}
</button>
);
};
အဓိက အားသာချက်ကတော့ state တစ်ခုကနေ နောက်တစ်ခုကို transition
လုပ်တဲ့နေရာမှာပါ။ များသောအားဖြင့် state တွေဟာ တစ်ခုနဲ့တစ်ခုဆက်နွယ်နေတတ်ပါတယ်။ ဥပမာ - counter example ကိုပဲ ပြန်ကြည့်ရင် ၃ ခါ click ပြီးရင် ၁၀ စက္ကန့် disable လုပ်ထားမယ်ဆိုတဲ့ logic မျိုးပေါ့။ ဒီလို Logic မျိုးမှာ condition တွေ ၂ ခု ၃ ခု ခံရတော့မှာပါ။ အကယ်၍ ဒီလိုမျိုးဆက်နွယ်နေတဲ့ state တွေ အများကြီးရှိမယ်ဆိုရင် သာမန် state management နဲ့ လုပ်တာထက် state machine တွေကို သုံးတာက ပိုပြီး manage လုပ်ရလွယ်ပါလိမ့်မယ်။
XState
နဲ့ Redux
မတူတဲ့အချက်တစ်ခုက XState
မှာ side effects
တွေအတွက် specification မှာတင်ကို built-in ပါပြီးသားဖြစ်ပါတယ်။ Redux လို third-party ဖြစ်တဲ့ thunk
တွေ saga
တွေမလိုပါဘူး။ ပြီးတော့ Redux က state update အတွက် pure function reducer တွေ သုံးရမယ်ဆိုတာကလွဲပြီး ကျန်တာကို dictate မလုပ်ပါဘူး။
State machine ကတော့ Learning curve နည်းနည်းမြင့်ပါတယ်။ Recoil နဲ့ တခြားဟာတွေမှာမရှိတဲ့ အချက်ကတော့ Portability ပေါ့။ ကျနော်တို့ React App တစ်ခုမှာဆောက်ခဲ့တဲ့ State update logic တွေပါတဲ့ Machine တွေကို တခြား app တွေ ၊ တခြား framework (ဥပမာ - Vue app) တွေမှာပါ ပြန် plug and play လုပ်လို့ရတဲ့အထိ လွယ်ပါတယ်။ နောက်ပြီး visualization ပေါ့။ ကိုယ့်ရဲ့ Logic ကို https://xstate.js.org/viz/ မှာ စမ်းပြီး မှန်မမှန် ကြည့်လို့ရတာမျိုးတွေက တော်တော်ကောင်းပါတယ်။
(State machine အကြောင်း နောက်မှ သပ်သပ်ရေးပါဦးမယ်။)
Conclusion
ကျနော့် ပုံမှန် approach ကတော့ များသောအားဖြင့် Redux
ကိုရှောင်ပြီးတော့ Data store အတွက် react-query
၊ NextJS
မှာဆိုရင်တော့ SWR
ကို သုံးပါတယ်။ API Data နဲ့ မဆိုင်တဲ့ shared state တွေမှန်သမျှအတွက် Context API + useReducer
ကို အများဆုံးသုံးဖြစ်ပြီး နည်းနည်းရှုပ်ထွေးတဲ့ နေရာတွေမှာတော့ Recoil
ကို ပဲသုံးဖြစ်ပါတယ်။
ဒီလောက်ဆိုရင် လက်ရှိ React world မှာ အများဆုံးသုံးနေတဲ့ state management strategy တွေကို တော်တော်များများ ခြုံငုံမိမယ်လို့ထင်ပါတယ်။