Redux in Depth: Tại sao immutability là bắt buộc với Redux?

Đã có khi nào bạn tự hỏi tại sao immutable state là bắt buộc trong Redux hay chưa? Hay đã có khi nào bạn vô tình mutate state và kết quả là UI không được render lại theo sự thay đổi của state chưa? => Nếu bạn đã từng rơi vào các tình huống đó thì hãy cùng mình "Deep read" một chút để tìm ra câu trả lời cho những câu hỏi đó nhé

1. Lợi ích của immutability là gì?

Immutability giúp cải thiện hiệu năng của app, và khiến cho việc lập trình cũng như debug trở nên đơn giản và dễ dàng hơn, bởi lẽ việc thay đổi dữ liệu một cách tùy tiện trong cả ứng dụng sẽ dẫn tới rất nhiều khó khăn trong việc debug.

Và đặc biệt hơn, immutability sẽ giúp cho các kỹ thuật tìm kiếm sự thay đổi sẽ phải xử lý nhẹ nhàng hơn, năng suất hơn, từ đó đảm bảo đạt được hiệu năng cao nhất có thế.

2. Tại sao immutability là bắt buộc đối với redux?

Nguyên nhân cốt lõi của vấn đề là ở việc Redux sử dụng shallow equality checking!!! 👀 À, ờ,... Ơ nhưng mà shallow equality checking là gì ấy nhỉ??? Ok so Let's go to 2.1 nào 👇

2.1. Khái niệm Shallow equality checking

👉 Shallow equality checking (hay so sánh tham chiếu) là phép so sánh chỉ cần kiểm tra xem 2 biến được so sánh có cùng tham chiếu đến 1 object hay không, ngược lại với nó là thuật ngữ deep equality checking (hay so sánh giá trị) phải kiểm tra mỗi giá trị của từng thuộc tính ở cả 2 objects. Như vậy, chỉ cần 2 biến cùng tham chiếu đến 1 object thì Shallow equality checking sẽ trả về giá trị là true, ngược lại, nếu chúng không cùng tham chiếu đến 1 object thì Shallow equality checking sẽ trả về giá trị là false (dù cho 2 biến có giống hệt nhau đi nữa mà không cùng tham chiếu đến 1 object thì Shallow equality checking vẫn sẽ trả về giá trị là false)

=> Do đó shallow equality check đơn giản hơn và nhanh hơn deep equality checking => Redux sử dụng shallow equality check để cải thiện performance Ngoài ra, điểm đặc biệt của shallow equality checking đó là: nó không thể phát hiện được rằng 1 object có bị thay đổi hay không sau khi được xử lý bởi 1 function nếu function đó thay đổi chính object được truyền vào cho nó. Ví dụ:

function mutateObj(obj) {
      obj.key = 'newValue'
      return obj
    }

const param = { key: 'originalValue' }
const returnVal = mutateObj(param)

param === returnVal 
//> true

Nguyên nhân là do 2 biến cùng tham chiếu tới 1 object sẽ luôn bằng nhau, nghĩa là bất kể các giá trị của object đó có bị thay đổi hay không, miễn là chúng cùng tham chiếu tới 1 object thì shallow equality checking sẽ luôn trả về giá trị là true.

2.2. Redux sử dụng shallow equality checking như thế nào?

Sử dụng trong function combineReducers

Redux sử dụng shallow equality checking trong hàm combineReducers để trả về hoặc một bản copy (đã được sửa đổi) của root state object, hoặc nếu không có thay đổi nào thì sẽ trả về root state object hiện tại.

Cụ thể:

Structure được đề nghị của Redux store là chia state object thành nhiều "slices", đồng thời khai báo các reducer function riêng biệt để quản lý các data slice đó.

combineReducers được tạo ra nhằm giúp thực hiện điều đó một cách dễ dàng thông qua việc truyền vào hàm này các argument là các cặp key/value, với mỗi key là tên của 1 state slice, và giá trị tương ứng là các reducer function của nó. Ví dụ:

function languageProviderReducer(state = initialState, action) {
  switch (action.type) {
    case CHANGE_LOCALE:
      return sth; (*)
    default:
      return state;
  }
}
combineReducers({
    route: routeReducer,
    language: languageProviderReducer
});

Trong đó:

  • route và language là key của các state slice;
  • Các giá trị routeReducer và languageProviderReducer là các reducer functions tương ứng của các key trên.

Khi đó combineReducers sẽ duyệt qua mỗi cặp key/value này, ở mỗi cặp, nó sẽ:

  • Tạo ra một tham chiếu đến state slice hiện tại của key đó (A)
  • Gọi tới reducer tương ứng và truyền tham chiếu đó vào.
  • Tạo ra một tham chiếu tới state slice được trả về bởi reducer này (B)

Tại mỗi giai đoạn của quá trình duyệt, combineReducers sẽ thực hiện một shallow equality check giữa state slice hiện tại và state slice được trả về bởi reducer (so sánh A và B). Nếu reducer trả về một object mới, shallow equality check sẽ mang giá trị fail (nghĩa là A khác B), và combineReducers sẽ gán cờ hasChanged thành true.

==> Như vậy: Theo structure của redux, ta thường tạo ra 1 reducer ở mỗi component và sau đó dùng combineReducers để tổng hợp các reducer này lại, vậy trong trường hợp ta mutate chính state object được truyền vào từ argument của reducer mà không tạo ra 1 bản copy khác để mutate thì dẫn tới việc reducer đó sẽ trả về state object B tham chiếu đến chính state object A, mà combineReducer lại sử dụng shallow equality check để so sánh A và B, nên nó sẽ thấy A==B và gán cờ hasChanged thành false => Giao diện không được render lại => Lỗi logic frontend.

Do đó, điều cực kỳ quan trọng bạn cần nhớ khi thao tác với state trong redux tại reducer đó là không bao giờ được mutate chúng, mà bắt buộc phải tạo ra một bản copy và thay đổi trên bản copy đó rồi return cho reducer function nhé :ok_hand_tone1:

=> Thật may mắn vì chúng ta có thể làm điều này một cách dễ dàng và hiệu quả với thư viện immutable.js

Ngoài ra, shallow equality checking còn được sử dụng trong method connect nữa (dùng để connect component với state thông qua function mapStateToProps), nhưng trong khuôn khổ bài viết này mình sẽ không đề cập tới, trong trường hợp nhiều bạn có hứng thú với nó thì mình sẽ viết thêm 1 bài viết nữa nói về shallow equality checking trong method connect ấy sau nhé 😝

Lưu ý quan trọng: immutability trong reducers cũng có thể khiến component render không cần thiết! But why??? => Hãy đến với phần 3 để biết thêm thông tin chi tiết nhé bạn :man_gesturing_no_tone1:

3. Immutability trong reducers có thể khiến component render không cần thiết như thế nào?

Bạn không thể mutate một immutable object, thay vào đó, bạn phải mutate trên một bản copy của nó.

Tuy nhiên, trong reducer, nếu bạn trả về một bản copy mà không hề có chút thay đổi nào, thì hàm combineReducers của Redux vẫn sẽ nghĩ rằng state cần được cập nhật, bởi bạn đã trả về một object khác với state slice object đã được truyền vào cho reducer đó.

combineReducers sẽ trả về 1 root state object mới tới store. Object mới này sẽ có cùng giá trị với root state object hiện tại, nhưng vì nó là 1 object khác nên nó sẽ dẫn tới việc update store, điều này cuối cùng sẽ dẫn tới tất cả các components liên quan sẽ bị re-rendered không cần thiết.

=> Do đó, để ngăn chặn việc này xảy ra, bạn luôn phải trả về state slice object đã được truyền vào cho reducer trong trường hợp reducer không thay đổi state (Đây cũng là lý do của đoạn code default: return state; phải được khai báo trong cú pháp switch-case ở reducer phía trên)

Nội dung nói trên cũng là phần cuối trong bài viết của mình, hi vọng rằng qua bài viết này các bạn đã nắm được lý do tại sao không được mutate state trong react-redux và có thêm những kiến thức thú vị xoay quanh chủ đề này, Thanks for reading

Tài liệu tham khảo

http://redux.js.org/ Bài viết liên quan đến chủ đề này mà bạn cũng có thể thích: http://reactkungfu.com/2015/08/pros-and-cons-of-using-immutability-with-react-js/


All Rights Reserved