Nếu bạn đang làm việc với React Redux mình tin bạn rất chán nản với việc làm mới lại state để cập nhật lại store. Với những state đơn giản thì việc này không mất quá nhiều thời gian, nhưng với những state có cấu trúc là object phức tạp nhiều lớp thì việc này rất chi là khổ sở 😐 . Nhưng đừng lo, bây giờ chúng ta đã có Immer, một thư viện nho nhỏ nhưng cực kì mạnh mẽ giúp chúng ta cập nhật lại state một cách đơn giản và dễ dàng hơn.

Để dễ nuốt bài này mình cho rằng bạn đã hiểu rõ về tham trị và tham chiếu Javascript nhé. Nếu chưa thì mình đã có một bài giải thích siêu chi tiết tại đây

Sơ lược về immer

Là người chiến thắng giải thưởng “Breakthrough of the year” React open source award và “Most impactful contribution” JavaScript open source award vào năm 2019.

Nghe thôi là biết thư viện đột phá và đáng dùng như thế nào rồi 😆 .

Immer là một package nhỏ cho phép bạn làm việt với các object bất biến một cách tiện lợi. Nó dựa vào cơ chếcopy-on-write (cái cơ chế này giải thích khá dài dòng, mà cũng không cần thiết phải đi sâu vào nó :mrgreen: ).

Ý tưởng là bạn sẽ thực hiện tất cả thay đổi trên một draftState tạm thời, đây là một proxy của currentState. Khi mà tất cả việt mutate xong xuôi thì Immer sẽ cho ra một nextState dựa trên những thay đổi trên draftState. Điều này có nghĩa là bạn có thể tương tác với data của bạn một cách đơn giản mà vẫn giữ được tính bất biến của data.

immer

Produce

Đây là hàm quan trọng nhất của Immer, mọi thứ sẽ đơn giản hơn với produce

import produce from "immer"

const todos = [ /* 2 object todo ở đây*/ ]

const nextTodos = produce(todos, draft => {
    draft.push({ text: "learn immer", done: true })
    draft[1].done = true
})

// state cũ không bị thay đổi
console.log(todos.length)        // 2
console.log(todos[1].done)       // false

// state mới đã được thay đổi từ draft
console.log(nextTodos.length)    // 3
console.log(nextTodos[1].done)   // true

// chia sẽ cấu trúc tham chiếu với nhau
console.log(todos === nextTodos)       // false
console.log(todos[0] === nextTodos[0]) // true
console.log(todos[1] === nextTodos[1]) // false

Hàm produce nhận vào 2 tham số. Tham số đầu tiên là state hiện tại, tham số thứ hai là hàm producer. Hàm producer nhận vào một tham số đó là draft, bất cứ khi nào bạn mutate draft kết quả sẽ được lưu lại và xuất ra nextState. currentState sẽ không bị thay đổi gì trong suốt quá trình này.

Nhờ vào việc sử dụng Proxy bên trong mà khi thao tác với produce, chúng ta có cảm giác như đang mutate trực tiếp object vậy.

Nâng cấp reducer của bạn với produce

Giả sử mình có một state như sau

const initialState = {
  products: [{
    id: '1',
    name: 'iphone'
  }],
  loading: false,
}

Và một reducer bình thường như sau

export const productReducer = (state = initialState, action) => {
  switch (action.type) {
    case 'ADD':
      return {
        ...state,
        products: [...state.products, action.payload],
      };
    case 'UPDATE':
      const _products = [...state.products];
      const index = _products.findIndex(
        (product) => product.id === action.payload.id
      );
      _products[index] = action.payload;
      return { ...state, products: _products };
    default:
      return state;
  }
};

Mô tả chút xíu: khi thực hiện hành động add thì ta thêm item vào products và khi thực hiện hành động update thì ta cập nhật lại products.

Object state của chúng ta khá đơn giản nhưng hãy nhìn thử code trong case: 'UPDATE' mà xem, để sửa một item trong products thì ta phải làm mới tham chiếu của state, products, products[index] (để tránh mutate trực tiếp state)

Và sau đây là productReducer khi áp dụng produce

import produce from 'immer';

export const productReducer = (state = initialState, action) =>
  produce(state, (draft) => {
    switch (action.type) {
      case 'ADD':
        draft.products.push(action.payload);
        break;
      case 'UPDATE':
        const index = draft.products.findIndex(
          (product) => product.id === action.payload.id
        );
        draft.products[index] = action.payload;
        break;
      default:
        break;
    }
  });

Code bây giờ ngắn gọn hơn rất nhiều, dễ đọc và dễ hiểu nữa. Đó chính là lợi ích vô cùng to lớn mà Immer mang đến cho chúng ta.

Nếu để ý thì khi dùng với produce chúng ta không cần return về một state mới trong switch case mà chỉ cần break thôi, vì việc return state mới đã có produce lo.

Mutate state, mutate state của bạn với produce

Nếu các bạn đã đọc bài Thay đổi mutate state React sao cho chuẩn thì sẽ thấy mình khuyên anh em dùng một thư viện nhỏ nhẹ đó là Immutability Helper . Nhưng khuyết điểm của thư viện này là cách dùng không được thuận mắt cho lắm. Còn đối với Immer thì bạn có thể thao tác như một Javascript object thông thường, vì thế mà nó vẫn giữ được type-checker tiện lợi.

import produce from "immer"

const todosObj = {
    id1: {done: false, body: "Take out the trash"},
    id2: {done: false, body: "Check Email"}
}

// add
const addedTodosObj = produce(todosObj, draft => {
    draft["id3"] = {done: false, body: "Buy bananas"}
})

// delete
const deletedTodosObj = produce(todosObj, draft => {
    delete draft["id1"]
})

// update
const updatedTodosObj = produce(todosObj, draft => {
    draft["id1"].done = true
})

Trình duyệt tôi không hỗ trợ Proxy

Proxy thuộc ES6 được hỗ trợ trên hầu hết trình duyệt gần đây. Các bạn có thể xem các trình duyệt hỗ trợ tại đây. Lưu ý là Immer sẽ không hoạt động trên IE. Nếu bạn muốn dùng Immer nhưng target source là ES5 thì có thể sử dụng import produce from "immer/es5", cú pháp sẽ giữ nguyên nhưng tốc độ sẽ bị chậm đi chút ít.

Cho bạn nào không rõ thì Proxy là API của ES6 và không thể compile sang ES5, vì thế Immer ES5 phải dùng giải pháp khác, không còn dùng Proxy nữa.

Hiệu suất của Immer

Theo mình không có lí do nào để bạn không dùng Immer cả. Nếu bạn đang lo lắng vềvấn đề hiệu suất của Immer thì đây là benchmarks.

  • Immer có tốc độ nhanh tương đương ImmutableJs, sở dĩ ImmutableJS + toJS chậm hơn rất nhiều so với ImmutableJS là vì bạn phải convert từ ImmutableJS object sang plain object để dùng!
  • Immer chậm hơn 2-3 lần so với dùng reducer viết tay truyền thống (điều này là không đáng kể nhưng bù lại sự tiện lợi vô cùng).
  • Immer ES5 thì chậm hơn một chút so với Immer ES6, vậy nên nếu target source của bạn là ES5 và bạn lo lắng thái quá về hiệu suất thì có thể bỏ qua Immer và chọn giải pháp khác.

immer performance

Tóm lại

Immer cung cấp cho chúng ta những tính năng tuyệt vời khi làm việc với dữ liệu bất biến trong Javascript. Trong bài này mình chỉ giới thiệu hàm produce, đây là hàm chính của Immer, ngoài ra còn một số hàm khác nữa các bạn tự khám phá nhé.

Mặc định khi làm việc với React mình sẽ cài đặt Immer, vì nó quá ngon. Còn anh em thì sao, nếu anh em có giải pháp khác thì có thể comment phía dưới để chia sẽ cho mọi người :mrgreen:

Tham khảo: https://medium.com/hackernoon/introducing-immer-immutability-the-easy-way-9d73d8f71cb3