រៀន redux-saga

ជាទូទៅ ពេលយើង update state តាមរយៈការ dispatch action អ្វីមួយទៅ reducer។ បន្ទាប់មក reducer នឹង check type របស់ action នោះ ដើម្បីដឹងថា តើយើងចង់ធ្វើអី រឺ ចង់ update store របៀបយ៉ាងម៉េច។ ឧទាហរណ៏ ចំពោះ real world application យើង request data from ពី remote server តាមរយៈ api. ពេលបាន data ហើយ យើង ប្រើ saga ដើម្បី dispatch action បន្តទៅអោយ reducer ដើម្បី update store នេះ ជា concept របស់ saga. saga ប្រើ generator function. Convention របស់ saga មានពីរគឺ watcher និង worker Watcher : ចាំមើល action ពេល dispatch Worker : ពេល Watcher ដឹងថា action នោះ ត្រូវបាន dispatch ហើយ, Watcher នឹង call ហៅ Worker អោយធ្វើការអីមួយដែលយើងចង់បាន ដូចជា request api ជាដើម + parallel vs. concurrent vs. async. vs. simultaneous concurrent: 2 queues, 1 coffee machines យើងអាចចាប់ផ្ដើម task ថ្មីមួយទៀត ដោយមិនចាំបាច់រង់ចាំ រឺ task ចាស់មិនទាន់ដំណើរការចប់ក៏ដោយ parallel: 2 queues , 2 coffee machines fork( process នៅតែភ្ជាប់ទៅនឹង process មេ ) vs. spawn( process ត្រូវបានផ្ដាច់ចេញពី process មេ ) ប្រសិនបើយើងមាន saga watcher ដែល watch action ផ្សេងគ្នាច្រើន យើងអាចបង្កើត multiple watchers ដែលដំណើរការនៅ background ឧទាហរណ៏៖ ``` function* fetchUsers(action) { ... } function* createUser(action) { ... } export default function* rootSaga() { yield takeEvery('FETCH_USERS', fetchUsers) yield takeEvery('CREATE_USER', createUser) } ``` Effect : គឺជា instruction ដែល middleware ប្រើដើម្បីដឹងថាគួរធ្វើការអីមួយ Saga អាច yield Effect បានច្រើនទម្រង់, តែទម្រង់ដែលយើង ឃើញញឹកញាប់ជាងគេ គឺ yield a Promise. call(fn, ...args) alias `apply()` សាកសមសំរាប់ function ដែល return លទ្ធផលជា promise cps(..args, (err, result) => {}) : style node សាកសមសំរាប់ i/o ``` const content = yield cps(readFile, '/path/to/file') ``` យើងអាចបោះ dispatch ទៅអោយ generator function បាន ``` function* fetchProducts(dispatch) { const products = yield call(Api.fetch, '/products') dispatch({ type: 'PRODUCTS_RECEIVED', products }) } ``` dipatch ពិបាកពេលត្រូវការ test ព្រោះត្រូវ mock ដូចនេះ saga មាន put Effect សំរាប់បញ្ហានេះ ``` function* fetchProducts() { const products = yield call(Api.fetch, '/products') yield put({ type: 'PRODUCTS_RECEIVED', products }) } ``` Error handling function* ...() { try{ } catch(error) { } } **Note Desired control flow ដោយគ្រាន់តែ Effects ផ្គុំ និង តំរៀបលំដាប់ Advance: Pulling future action takeEvery: spawn task ថ្មីពេល រាល់ពេល action អីមួយបានកើតឡើង ``` function* watchAndLog() { yield takeEvery('*', function* logger(action) { const state = yield select() console.log('action', action) console.log('state after', state) }) } ``` * takeEvery watcher call ហៅ task ដែល task នោះ មាន argument ជា action ក្នុងករណីនេះ តែផ្នែកមុន dispatch។ ប្រហែល៖ អត់ទាន់ test គ្រាន់តែមើល doc ហើយស្វែងយល់ឃើញចឹង 1. ពេល takeEvery('*', ...) => arg វា ជា action 2. តែពេល takeEvery('SPECIFIC', ...) => arg វា ជា dispatch ``` function* watchAndLog() { while (true) { const action = yield take('*') const state = yield select() console.log('action', action) console.log('state after', state) } } ``` * take() ដូចគ្នាហ្នឹង call() ហើយនិង put() ដែរ វា នឹងប្រាប់ middleware អោយរង់ចាំ action អីមួយ dispatch * call() ប្រាប់ middlware អោយ suspend generator រហូត ទាល់តែ Promise resolve ចំនែក * take() ប្រាប់ middleware អោយ suspend generator រហូត ទាល់តែ action ហ្នឹង ត្រូវបានគេ dispatch ```` function* watchFirstThreeTodosCreation() { for (let i = 0; i < 3; i++) { const action = yield take('TODO_CREATED') } console.log('Congrats!') } ```` មានន័យថា saga ចាំមើល action `TODO_CREATE` dispatch 3 ដងប៉ុណ្ណោះ ហើយ វានឹងចេញ message ថា `Congrats!` បន្ទាប់មក Generator នឹង gabage collected និងលែង observe action នោះតទៅទៀតដែរ។ term `run-to-complete` term `pull model` ```` function* loginFlow() { while (true) { yield take('LOGIN') yield take('LOGOUT') } }

Non-blocking call dispatch({ type: 'LOGIN_REQUEST', user: 'radinr', password: 'Q1p2m3g4' }) task = yield fork(fn) return task yield take(fn, ...) return action cancel(task) : ដើម្បី abort task ហ្នឹង try { } catch(error) { } finally { if( yield cancelled()) { } } function* authorize(user, password) { try { const token = yield call(Api.authorize, user, password) yield put({type: 'LOGIN_SUCCESS', token}) yield call(Api.storeItem, {token}) return token } catch(error) { yield put({type: 'LOGIN_ERROR', error}) } finally { if (yield cancelled()) { } } } function* loginFlow() { while (true) { const {user, password} = yield take('LOGIN_REQUEST') const task = yield fork(authorize, user, password) const action = yield take(['LOGOUT', 'LOGIN_ERROR']) if (action.type === 'LOGOUT') yield cancel(task) yield call(Api.clearItem, 'token') } } Running task in Parallel const users = yield call(fetch, '/users'), repos = yield call(fetch, '/repos') const [users, repos] = yield all([ call(fetch, '/users'), call(fetch, '/repos') ]) ពេលដែលយើង yield array នៃ effects, generator នឹងត្រូវបាន blocked រហូតដល់ effects ទាំងអស់ resolved រឺក៏មាន មួយណា rejected Starting `a race` between multiple effects race() Effect: ដំណើរការ tasks លក្ខណៈ parallel តែ វាមិននៅចាំ លុត្រាតែ tasks ទាំងអស់ចប់ទេ វាគ្រាន់តែចាំ task ណា ដែល resolve រឺក៏ reject មុនគេបង្អស់ ```` function* fetchPostsWithTimeout() { const {posts, timeout} = yield race({ posts: call(fetchApi, '/posts'), timeout: call(delay, 1000) }) if (posts) put({type: 'POSTS_RECEIVED', posts}) else put({type: 'TIMEOUT_ERROR'}) } ```` ប្រយោជន៏មួយទៀតរបស់ race() គឺវានឹង cancel tasks ដែលចាញ់ទាំងអស់ចោលដោយស្វ័យប្រវត្តិ។ ឧទាហរណ៏ ```` function* watchStartBackgroundTask() { while (true) { yield take('START_BACKGROUND_TASK') yield race({ task: call(backgroundTask), cancel: take('CANCEL_TASK') }) } } ```` ក្នុងករណីដែល action `CANCEL_TASK` ត្រូវបាន dispatch, race() នឹង ធ្វើការ cancel task `backgroundTask` ដោយស្វ័យប្រវត្តិ និងបោះ cancellation error នៅក្នុង backgroundTask ផងដែរ Sequencing sagas using `yield*` maybe it's a bit simple concept to implement Composing sagas