Nội dung bài viết
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ó ).
Ý 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.
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.
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
Tham khảo: https://medium.com/hackernoon/introducing-immer-immutability-the-easy-way-9d73d8f71cb3
immer có hoạt động trên react-native android rồi mà nhỉ
Mình vừa check lại thì thấy có support rồi. Cảm ơn bác nhá
Bác ơi ra bài cài đặt, config, build,env,deploy… 1 project reactjs thực tế từ a->z được không ạ. Em cảm ơn bác nhiều ạ
đó là quá trình dài lắm bác. Mình sẽ viết vài bài về nó
dạ vâng thanks bác ạ. E muốn tự tạo 1 project thực tế mà nhiều thứ quá ạ :))
Thường đi phỏng vấn họ hay hỏi khi mình dùng redux để xử lý update deeply nested state thì phải làm thế nào, hồi đó ngu mới xài redux chưa dùng immer nên bảo không biết.
https://www.pluralsight.com/guides/deeply-nested-objectives-redux