Writer at fiveminutes.today

May 6, 2018, 7:13 p.m.

React và Redux đơn giản cho người mới bắt đầu ! (Phần 3)

Đọc các bài cũ hơn:

Phần 1: Nguyên lý cơ bản Redux

Phần 2: Redux + React Native

Hey guys, ở 2 phần trước, chúng ta đã hiểu và cài đặt Redux dùng với React Native, giờ là đến phần quan trọng nhất của Redux, cái mà chúng ta gần như sẽ dùng nó liên tục trong dự án thực tế, đó chính là middleware.

Middleware là gì?

Xét ví dụ: ở bài 2 khi bấm nút INCREASE thì bộ đếm counter ngay lập tức tăng lên 1 đơn vị:


export const counterIncrease = () => ({type: INCREASE});

Giờ bài toán đặt ra là: muốn bấm vào button increase nhưng sau 1s bộ đếm counter mới tăng lên 1 đơn vị, vậy phải xử lý ntn?

Chúng ta thử modify cái action ở trên thành như này liệu ok ko nhé?

export const counterIncrease = () => {
setTimeout(() => {
return {type: INCREASE};
}, 1000);
};

Nhìn qua thì ok đấy, nhưng bạn thử xem, code chạy lập tức báo lỗi vì sao?

vì đơn giản là tác giả Redux nói rằng :)).

Actions must be plain objects, use custom middleware for async actions.

Nghĩa là bạn ko bao giờ được phép viết “linh tinh” vào thân hàm action kia vì action phải là 1 plain object.

Bạn sẽ hỏi tại sao là 1 plain object?

Câu trả lời là nếu bạn làm mọi thứ theo cách đơn giản thì khi gặp vấn đề bạn cũng dễ dàng tìm ra nguyên do của nó.

plain object mô tả hành động xảy ra theo cách clear nhất, nhìn vào object đó nó giúp bạn hiểu ngay được những gì đang diễn ra trong app và tại sao nó thay đổi.

Để giải quyết các vấn đề phát sinh, râu ria lằng ngoằng này, Redux cung cấp 1 thằng mang tên middleware có nhiệm vụ tạo ra side-effect và xử lý trước khi gọi action.


Về cơ bản nó là 1 bước trung gian như đúng cái tên của nó nghĩa là nhận các action đầu vào rồi và trả ra cũng là các action.

Hiện nay redux đang có khá nhiều nhiều thư viện middleware bao gồm:

Redux-saga , redux-promise, redux-effects, redux-thunk, redux-connect, redux-loop, redux-side-effects, redux-logic, redux-observable, redux-ship

Nhưng nói chung trong đám kể trên thì chỉ có 3 cái tên xuất chúng nhất, được dùng phổ biến là:

  • Redux-Saga
  • Redux-Thunk
  • Redux-Observable

Chúng có ưu nhược điểm như thế nào thì ta sẽ cùng nhau phân tích từng thằng một ngay sau đây:

1. Redux-Thunk

Thunk là gì?

Thunk là 1 function mà nó khác biệt những function bình thường là thay vì return trực tiếp kết quả thì thunk lại return ra 1 function và trong function đó làm tiếp một vài nhiệm vụ nữa sau đó mới return ra kết quả cuối cùng.

Ví dụ:

const addNumber = function(a){
console.log("sum");
return function(b){
return a + b;
}
}

Trong redux-thunk áp dụng nguyên lý của Thunk để return 1 function có 2 tham số getState và dispatch 2 tham số này chính là 2 thuộc tính của store trong Redux. Bằng cách này thì redux-thunk cho phép ta tạo ra side-effect ( như fetch data, delay request …) sau đó mới dispatch 1 action plain object.

Ví dụ sau là call 2 api liên tiếp để có được kết quả, bằng cách dùng async/await của ES7 giúp tránh được cách viết nồng nhau Promise-Hell


const getUserById = id => async (getState, dispatch) => {
try{
const {token} = await callGetUserApi(id);
const response = await callGetReportApi(token);
const report = JSON.parse(response.report);
dispatch({
type:"GET_REPORT_SUCCESS",
payload:report
});
}catch(error){
dispatch({
type:"GET_REPORT_FAIL",
payload:{message:"fail to get report"}
});
}

}

Config trong project với React

import {createStore, applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import reducers from '../reducers';
const store = createStore(
reducers,
{},
applyMiddleware(thunk)
);
export default store;

Nhìn chung redux-thunk là khá dễ hiểu, dễ code nhưng chưa thật sự mạnh mẽ trong nhiều tình huống như sau:

  • Tạm dừng 1 Request hoặc hủy request khi đang call api.
  • Bài toán click vào Button để fetch data nếu click liên tục nhưng chỉ lấy lần click sau cùng?
  • Bài toán Auto-Search, tự động hiển thị kết quả sau mỗi lần gõ text, để tránh request server liên tục thì yêu cầu sau 1 khoảng thời gian ví dụ 2s thì mới thực hiện request, hoặc gõ từ khóa mà nó trùng với từ khóa trước thì ko request lại.
  • Tự động Retry request một vài lần khi có sự cố ví dụ như sự cố mạng xảy ra?

… và còn nhiều yêu cầu khác phức tạp hơn nữa sau này các bạn làm project sẽ gặp phải.

Để khắc phục những vấn đề nêu trên thì chúng ta phải tìm đến công cụ mạnh mẽ hơn và trong số này mình recommend 2 cái tên thích hợp là:

Redux-Saga hoặc Redux-Observable

2. Redux-Saga

Để nắm được Redux-Saga hoạt động như thế nào thì bạn phải hiểu được cách sử dụng Generator function của ES6.

Khác với Redux-thunk, thì Redux-saga tạo ra phần side-effect độc lập với actions và mỗi action tương ứng sẽ có 1 saga tương ứng xử lý.

Config trong store:

import {createStore, applyMiddleware} from 'redux';
import reducers from '../reducers';
import createSagaMiddleware from 'redux-saga';
import rootSaga from '../sagas';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
reducers,
{},
applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(rootSaga);
export default store;

Chúng ta cần combine tất cả các sagas vào một file rootSaga như sau:

import {watchIncrementAsync} from './counterSaga';
import {watchFetchUser} from "./userSaga";
export default function* () {
yield [
watchIncrementAsync(),
watchFetchUser()
]
}

Về tư tưởng thì saga là: Saga = Worker + Watcher

Ví dụ dưới đây có worker là fetchUser() làm nhiệm vụ get data sau đó dispatch 1 action trong khi đó 1 watcher có tên watchFetchUser() sẽ có nhiệm vụ liên tục đón nhận action được phát ra ở View và gọi tới worker tương ứng để xử lý.

const fetchUserApi = (delay) => new Promise(resolve => {
setTimeout(() => {
resolve({
name: "Chris Ho",
position: "Front-end developer",
email: 'tridungbk@gmail.com'
})
}, delay)
});
function* fetchUser() {
try {
const response = yield call(fetchUserApi, 1000);
yield put({type: FETCH_USER_SUCCESS, payload: response});
} catch (err) {
yield put({type: CANCEL_FETCHING_USER});
}
}
export function* watchFetchUser() {
while(yield take(FETCH_USER)){
const getUser = yield fork(fetchUser);
yield take(CANCEL_FETCHING_USER);
yield cancel(getUser);
}
}

Saga có syntax hơi lạ chút nên ban đầu khi tiếp cận, mình không thấy thoải mái cho lắm :D, nhưng sau dùng 1 time quen thì khá ok đặc biệt là luồng code tự nhiên giống với ngôn ngữ con người hơn Redux-Thunk.

  • Call (Gọi tới api hoặc 1 Promise, có truyền tham số)
  • Fork: rẽ nhánh sang 1 generator khác.
  • Take: tạm dừng cho đến khi nhận được action
  • Race: chạy nhiều effect đồng thời, sau đó hủy tất cả nếu một trong số đó kết thúc.
  • Call: gọi function. Nếu nó return về một promise, tạm dừng saga cho đến khi promise được giải quyết.
  • Put: dispatch một action. (giống như dispatch của redux-thunk)
  • Select: chạy một selector function để lấy data từ state.
  • takeLatest: có nghĩa là nếu chúng ta thực hiện một loạt các actions, nó sẽ chỉ thực thi và trả lại kết quả của của actions cuối cùng.
  • takeEvery: thực thi và trả lại kết quả của mọi actions được gọi.

Ưu điểm:

  • Do tách riêng side-effect ra khỏi action nên việc testing là dễ dàng hơn Redux-Thunk.
  • Giữ cho action pure synchronos theo chuẩn redux và loại bỏ hoàn toàn callback theo javascript truyền thống.

Nhược điểm:

  • Function ko viết được dạng arrow-function.
  • Phải hiểu về Generator function và các khái niệm trong saga pattern
  • Cách viết theo lối Imperative

3. Redux-Observable

Redux-Observable được xây dựng dựa trên thư viện ReactiveX cho JavaScript, một thư viện vô cùng mạnh mẽ là sự kết hợp giữa ý tưởng Observer pattern với Iterator pattern và functional programming.

Rx được dùng hầu như cho tất cả các ngôn ngữ hiện nay như: C#, Java, Swift, Python, Golang…vv. do vậy bạn sẽ cảm thấy quen thuộc và tìm thấy tiếng nói chung khi discuss về 1 vấn đề với các ngôn ngữ khác.

Về config thì cơ bản là giống Redux-saga, cũng tách riêng side-effect ra để xử lý, ở Redux-Saga ta có các saga thì Redux-Observable chúng là các epics

Epics là gì?

It is a function which takes a stream of actions and returns a stream of actions. Actions in, actions out.

function (action$: Observable<Action>, store: Store): Observable<Action>;

Mọi thứ sẽ quy về stream of action ( Dòng chảy). Chi tiết RxJS và tư tưởng stream of actions mình sẽ viết một bài chi tiết sau. Bởi khi bạn hiểu được tư tưởng này thì nó sẽ giúp bạn làm mọi việc cự kỳ dễ dàng và nhanh chóng.

Config với React

import {createStore, applyMiddleware} from 'redux';
import reducers from '../reducers';
import { createEpicMiddleware } from 'redux-observable';
import rootEpic from '../epics';
const epicMiddleware = createEpicMiddleware(rootEpic);
const store = createStore(
reducers,
{},
applyMiddleware(epicMiddleware)
);
export default store;

Redux-observable dùng combineEpics để tạo ra 1 file rootEpic duy nhất:

import {combineEpics} from 'redux-observable';
import {counterEpic} from './counterEpic';
import userEpic from './userEpic';
export default combineEpics(
counterEpic,
userEpic
)

Tư tưởng viết Redux-observable: Epic( Type + Operators )

Ví dụ: 1 Đoạn code minh họa việc bấm counter tăng tới 30 trong vòng 1s rồi dừng lại, trong khi tăng có thể bấm reset về 0. (Các bạn có thể checkout ví dụ đầy đủ này trên git source code).

import {DECREASE, INCREASE, STOP_COUNTER, FETCH_USER, INCREASE_DONE} from '../actions/type';
import {Observable} from 'rxjs';
export const counterEpic = action$ =>
action$.ofType(INCREASE)
.mergeMap(() => Observable.timer(0,30)
.takeUntil(Observable.timer(1000))
.map(() => ({type:INCREASE_DONE}))
.takeUntil(action$.ofType(STOP_COUNTER))
);

Ví dụ 2: fetchUser()

import * as actionTypes from '../actions/type';
import {Observable} from 'rxjs';
const fakeApi = () => Observable.of({
userId: 1,
name: "chris",
position: "Front-end",
email: "chris.ho@innovatube.com"
}).delay(2000);
export default action$ => action$.ofType(actionTypes.FETCH_USER)
.mergeMap(() => fakeApi()
.map(response => ({
type: actionTypes.FETCH_USER_SUCCESS,
payload: response})
).takeUntil(action$.ofType(actionTypes.CANCEL_FETCHING_USER))
);
  • Giải thích ý nghĩa các operator ở ví dụ trên:
  • mergeMap: với mỗi lần call api thì nó sẽ map với action:
  • map: giống trong ES kiểu trả ra là plaint object
  • takeUntil (dùng khi muốn cancel action)

Ngoài ra còn rất nhiều operators nữa nên bạn cần xem thêm:

http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-mergeMap

So sánh Saga và Observable

Giữa Redux-saga và Redux-Observable thì mình đánh giá mạnh mẽ như nhau cá nhân mình cực kỳ thích ở Redux-observable là code cực kỳ xúc tích và nó là Declarative, hơn thế nữa nếu dùng Observable thì việc transfer giữa các nền tảng (front-end) là rất tốt. (Với RxSwift cho iOS, hay RxJava cho android).

Ví dụ cùng thực hiện 1 việc fetchUser() nhưng Observable xúc tích hơn :D

Để có cái nhìn khách quan và sâu sắc hơn nữa, bạn nên đọc thêm bài viết sau:

https://hackmd.io/s/H1xLHUQ8e

Kết

Tóm lại trong 3 thằng middleware thì tùy vào yêu cầu bài toán và độ phức tạp mà ta lựa chọn middleware nào phù hợp, với những bài toán đơn giản ít xử lý side-effect thì chọn Thunk, phức tạp hơn thì dùng Redux-Saga hoặc Redux-Observable.

Okay fine! Vậy là chúng ta đã đi qua loạt bài về sử dụng Redux, mình hy vọng các bạn sẽ có cái nhìn rõ ràng về Redux để có thể dễ dàng áp dụng nó vào project một cách hiệu quả.

Source code

https://github.com/tridungbk2010/react-native-class/tree/master (checkout thunk, saga, observable theo từng branch tương ứng)

Khuyến khích người viết

Bằng cách chia sẻ bài viết của :5 Phút Mỗi Ngày.

0
Writer at fiveminutes.today

May 6, 2018, 7:13 p.m.

0 Comment

Register for News