Redux và MobX-State-Tree trong React Native

Chúng ta có 2 vấn đề với state của ứng dụng trong lập trình di động:

  • 1 component muốn chia sẻ state với 1 component khác.
  • 1 component muốn thay đổi state của 1 component khác.

Ứng dụng nhỏ thì cách giải quyết đơn giản là truyền state qua lại giữa những component và muốn thay đổi state giữa chúng, ta có thể dùng ref(reference), nhưng với ứng dụng với độ phức tạp cao, 1 state muốn dùng ở nhiều component thì sao? Làm như trên sẽ rất khó kiểm soát state, mà có khi lại còn gây ra 1 số bug.

Và chúng ta cũng đã nghe nói đến Redux, hay xuất hiện muộn hơn là MobX-State-Tree(MST), chúng đều được viết bằng Javascript và được sinh ra để giải quyết 2 vấn đề trên. Sau 1 thời gian làm việc với cả 2 thư viện trên và từ 1 số bài viết, mình xin phép so sánh 1 chút giữa 2 thư viện này.

1. Số lượng Store

Redux tuân theo nguyên tắc Single source of truth(chỉ có 1 nơi lưu trữ trạng thái(state) của ứng dụng). Vì vậy, Redux chỉ sử dụng 1 Store duy nhất và chia thành nhiều reducer để quản lý state.

Còn MST, bạn có thể sử dụng nhiều hơn 1 store để lưu trữ state, hoặc cũng có thể gom hết các store vào 1 RootStore cho dễ quản lý và sử dụng(giống nguyên tắc của Redux).

2. Immutable, mutable

Redux, dữ liệu được lưu trong store là bất biến(Immutable), chúng ta không thể thay đổi trực tiếp state trong store mà luôn trả về 1 state mới bằng cách sử dụng 1 số phương pháp copy state như Object.assign(), Spread Operator hoặc các thư viện hỗ trợ như Immutable, Immutable-helper, ...

// type of action
const ADD_USER = 'ADD_USER'

// define User
interface User {
  id: number
  name: string
  age: number
}

// define add user action
interface AddUserAction {
  type: string
  item: User
}

const listUser: User[] = []

export const doAddUser = (state: User[] = listUser, action: AddUserAction) => {
  switch (action.type) {
    case ADD_USER:
      // Không thay đổi trực tiếp state như:
      // return state.push(action.item)
      // Dùng Spread Operator để tạo ra dữ liệu mới như sau:
      return [...state, action.item]
    default:
      return state
  }
}

MST thì khác, dữ liệu được lưu là dạng tuỳ biến(mutable), có thể thay đổi được và thay đổi trực tiếp như sau:

import { Instance, types } from "mobx-state-tree"

const User = types.model({
  id: types.number,
  name: types.string,
  age: types.number,
})

interface UserProps extends Instance<typeof User> {}

export const ListUserModel = types
  .model("ListUser")
  .props({
    listUser: types.optional(types.array(User), []),
  })
  .actions((self) => ({
    doAddUser: (item: UserProps) => {
      self.listUser.push(item)
    },
  }))
3. Xử lý bất đồng bộ

Redux sử dụng hàm thuần khiết(Pure Function), là một hàm nhận input, trả về output và không có thêm bất cứ sự phụ thuộc nào khác, và không sử dụng async, await để xử lý bất đồng bộ được.

(state, action) => newState

Vì vậy khi xử lý bất đồng bộ, cần dùng thêm middleware như Redux-Thunk, Redux-Saga, Redux-Observable hỗ trợ. Dưới đây là ví dụ với Redux-Saga(lấy từ README.md của Redux-Saga và sửa lại).

import { put, takeEvery, call } from "@redux-saga/core/effects"
import { GET_USER_INFO_SUCCEEDED, GET_USER_INFO_FAILED, GET_USER_INFO_REQUESTED } from "../types/action-types"

interface Response {
  data: {
    id: number;
    name: string;
    age: number;
  };
  message: string;
  success: boolean;
}

function* doGetUserInfo(userId: number) {
  const response: Response = yield call(API.getUserInfo, userId);
  if (response.success) {
    yield put({ type: GET_USER_INFO_SUCCEEDED, user: response.data });
  } else {
    yield put({ type: GET_USER_INFO_FAILED, error: response.message });
  }
}

function* mySaga() {
  yield takeEvery(GET_USER_INFO_REQUESTED, doGetUserInfo);
}

export default mySaga;

MST không sử dụng Pure Function, thay vào đó ta dùng flow để xử lý:

import { applySnapshot, flow, types } from "mobx-state-tree"

const User = types.model({
  id: types.optional(types.number, 0),
  name: types.maybeNull(types.string),
  age: types.optional(types.number, 1),
})

const Response = types.model({
  data: types.optional(User, {}),
  message: types.maybeNull(types.string),
  success: types.optional(types.boolean, false),
})

export const ListUserModel = types
  .model("ListUser")
  .props({
    user: types.optional(User, {}),
  })
  .actions((self) => ({
    doGetUserInfo: flow(function * (userID: number) {
      const response: Response = yield Api.getUserInfo(userID)
      if (response.success) {
        self.user = response.data
      } else {
        applySnapshot(self.user, {})
      }
    }),
  }))
4. Re-render

Redux sử dụng đối tượng Javascript để lưu trữ state và việc re-render sẽ luôn được thực hiện khi reducer chịu tác động bởi action dù state có sự thay đổi hay , gây ra việc render thừa, muốn kiểm soát việc đó thì chẳng có cách nào ngoài tự bạn làm.

MST được phát triển từ MobX và được kế thừa việc sử dụng Observable(Observable?) để lưu dữ liệu, khi dữ liệu có sự tác động và thay đổi thì component sẽ tự động re-render, còn khi có tác động mà không có sự thay đổi thì component sẽ không tự động re-render.

5. Khả năng tiếp cận

Với những ví dụ trên, ta có thể thấy MST dễ để sử dụng với người mới hơn, khái niệm phải làm quen khi mới bắt đầu ít hơn, viết ít code hơn, việc re-render đã có MobX lo.

Còn với Redux thì có lẽ là 1 câu chuyện khá dài, từ viết code, hiểu định nghĩa của reducer, action creator,... xử lý re-render và còn nhiều thứ khác mà ban đầu học, bạn mất kha khá thời gian để tìm hiểu.

6. Debug

Trong Redux, muốn thay đổi 1 state, thay thông qua việc dispatch 1 action tương ứng và cho bạn có quyền để can thiệp vào từng action, log ra từng thứ bạn viết, và bạn có toàn quyền để kiểm soát state và việc re-render của ứng dụng.

Còn MST, việc debug khá khó khăn nếu gặp trường hợp lỗi với re-render vì như đã nói ở trên thì việc re-render do MST kiểm soát, chúng ta không thể điều tra từng tầng nhỏ như Redux được. Bù lại, MST có actions, và có thể thay đổi state bằng actions giống như Redux.

7. Sự hỗ trợ từ cộng đồng

So sánh về tuổi đời, Redux ra sớm hơn MST và đã gây dựng được lòng tin với cộng đồng người dùng, lẽ dĩ nhiên là được hỗ trợ rất nhiều từ các lập trình viên trên thế giới.
Với MST, cộng đồng người dùng còn ít nên sẽ khá khó để hỏi khi debug mà gặp những lỗi quái dị.

Qua bài viết này, hy vọng bạn đã có cái nhìn tổng quan về 2 thư viện Redux và MobX-State-Tree trong việc quản lý state và có thể cân nhắc, áp dụng chúng vào dự án của mình.

Cảm ơn bạn đã đọc bài viết và nếu có sai sót, xin vui lòng góp ý bên dưới.

Tài liệu tham khảo: