Xây dựng thư viện Redux cho ứng dụng ToDo
Giới thiệu
Redux là thư viện JavaScript mã nguồn mở dùng để quản lý state (trạng thái) của các ứng dụng JavaScript. Redux thường được sử dụng cùng với một số thư viện JavaScript khác như React hay Angular để xây dựng front-end cho một ứng dụng Web (Redux không phụ thuộc vào React hay Angular). Redux được phát triển dựa trên kiến trúc của Flux (Facebook), được tạo ra bởi Dan Abramov và Andrew Clark. (hai cậu này đều làm trong core team của ReactJS!)
Nguyên lý của Redux dựa trên các khái niệm về functional programming và reducing function (Wikipedia).
Bài viết này giới thiệu về Redux theo từng bước sau đây:
- Tìm hiểu nguyên lý và khái niệm cơ bản của Redux (lý thuyết)
- Dựa trên các nguyên lý này, xây dựng một thư viện đơn giản có chức năng tương tự Redux (tạm gọi, Naive Redux)
- Tích hợp Naive Redux vào ứng dụng ToDo đã được giới thiệu qua một bài viết về React.
- Thay đổi Naive Redux bằng thư viện Redux của Dan Abramov, và áp dụng một số tính năng nâng cao (phần này tương đối dài, nên sẽ tách thành một bài viết riêng tại đây (a placeholder for link)
Source code của chương trình có thể tham khảo tại Github (checkout branch: naive-redux)
Khái niệm
Redux là một thành phần container để quản lý các trạng thái có thể đoán định cho các ứng dụng JavaScript.
( Predictable state container for JavaScript apps — http://redux.js.org)
Cụm tử “đoán định” — predictable state — áp dụng khi Redux không sử dụng để quản lý state với các side effect như thực hiện các async request đến server side. Trong trường hợp này, Redux cần kết hợp với các thư viện khác như Redux Thunk hay Saga
Trước khi bắt đầu vào việc xây dựng một thư viện riêng theo mô hình Redux, chúng ta cần tìm hiểu một số các khái niệm cơ bản trong Redux.
Redux được sử dụng để quản lý state (trạng thái) của một ứng dụng JavaScript. State bao gồm hai thông tin đầu vào:
- Giá trị được gửi về từ server side
- Các tương tác với người dùng trên giao diện ứng dụng (UI application)
Redux sử dụng một đối tượng JavaScript gọi là store
để quản lý state. Đối tượng store
cung cấp các phương thức để đọc hoặc cập nhật giá trị của state
.
Nguyên tắc hoạt động của Redux dựa trên mô hình Publish/Subscribe (aka, Pub-Sub).
Mô hình PubSub, là một trong những phương pháp phổ biến áp dụng trong kiến trúc thiết kế phần mềm theo hướng sự kiện (Event-Drivent Architecture).
Khi người dùng thực hiện một tương tác với ứng dụng, hoạt động này sẽ tạo ra một đối tượng action gửi đến (dispatch) store. Action được thể hiện đơn giản thông qua một JavaScript object (hoặc một promise) với hai thuộc tính:
type
: mô tả loại action thông qua các const string (ví dụ: ADD_TODO, REMOVE_TODO)payload
: thông tin đi kèm action (ví dụ, nội dung của ToDo item, key của ToDo item)
Action được tạo ra bởi function (tạm gọi là action creator). Action creator có cấu trúc rất đơn giản. Ví dụ:
Chú ý:
- Việc dispatch một action có thể xảy ra bởi nhiều lý do (bao gồm, tương tác trên UI của người dùng). Một action khi gửi đi, cũng có thể là nguyên nhân dispatch cho nhiều action kế tiếp.
- Việc sử dụng function, action creator, không có tính bắt buộc. Các React component có thể dispatch trực tiếp action đến store. Tuy nhiên action creator giúp cho việc tách biệt các vai trò trong một ứng dụng cùng tính tái sử dụng source code tốt hơn.
Khi một action được gửi đến store, store sẽ sử dụng các hàm reducer (reducing function), dựa trên thông tin của type
và payload
của action, kết hợp cùng state hiện tại, và sản sinh ra state mới. Ví dụ: tạo ra một danh sách ToDo item (cụm từ “tạo” được sử dụng thay cho “cập nhật” để chỉ rằng state trong ứng dụng React cần theo nguyên tắc immutable data trong functional programming).
Bên cạch việc gửi đi (dispatch) action, các component có thể đăng kí (subscribe) với sate của ứng dụng thông qua đối tượng store
. Bằng cách này, khi giá trị của state thay đổi, các component tương ứng có thể được thông báo (publish) và thực hiện các thanh đổi UI tương ứng. Ví dụ, cập nhật danh sách ToDo item trên giao diện khi người dùng bổ sung thêm một ToDo item.
Có thể minh hoạt quá trình này
Với mô hình trên, các nguyên lý của Redux bao gồm:
- Single source of truth: toàn bộ state của ứng dụng được lưu trong đối tượng store
- Ready-only state: state chỉ có thể được đọc ra, và không được phép thay đổi trực tiếp
- Pure function: các hàm reducing function (reducer) là những pure function. Khi thao tác với state, reducer không được phép sửa đổi state. Thay vào đó, reducer dựa trên giá trị state cũ, cộng với dữ liệu đầu vào để tạo nên đối tượng state mới.
Xây dựng Naive Redux
Có thể thấy rằng các xử lý của Redux tập trung trong đối tượng store (store thực hiện các hoạt động liên quan đến Redux, trong khi reducer thực hiện các xử lý liên quan đến logic dữ liệu). Đối tượng store
cần cung cấp những thuộc tính và phương thức sau:
- Lưu trữ
state
của ứng dụng - Cung cấp
subscribe
cho phép các React component đăng kí để nhận thông báo khi state thay đổi - Phương thức
subscribe
đi kèm một danh sách cáclistener
. Bản chất là một danh sách các function callback của các subscriber (React component), được gọi (nhận thông báo) khi state thay đổi. - Cung cấp phương thức
getState
cho phép đọc (read-only) giá trị state bên trong store Store
cũng liên kết vớireducer
, để thực hiện việc thay đổi state tương ứng khi nhận được action
Tham khảo chi tiết hơn về đối tượng store: GitHub
Bài tập
Hãy giải thích vì sao trong source code trên, chúng ta thực hiện việc dispatch INIT action trong quá trình khởi tạo store ?
Sau khi xây dựng store, chúng ta cần định nghĩa các action cần dispatch đến store. Trong ví dụ về ToDo list, chúng ta tạo ra một action createAddTodo
như sau:
Action creator, createAddTodo
có một chút khác biệt nhỏ so với ví dụ đưa ra trong phần đầu. Chúng ta chuyển khai báo các action type
sang một file constant (actions.js
). Các liên kết khác đến action type
(ví dụ bên trong reducer function) đều thông qua actions.js
, cách làm này giúp cho việc khai báo các action type được tập trung hơn và tránh những lỗi về typo (đánh nhầm tên action).
Tiếp tục chú ý về logic trong source code của createStore.js
(Line 6), dối tượng store
được khởi tạo với hai giá trị đầu vào:
- Giá trị khởi tạo
state
của ứng dụng (initialState
) - Hàm
reducer
Bắt đầu với giá trị khởi tạo state (initialState
) bằng cách xây dựng function createInitialState
:
Một lợi ích của việc xây dựng hàm createInitialState
là giúp cho người đọc có thể hiểu được cấu trúc state (State Shape) của một ứng dụng. Trong ví dụ này, chúng ta hiểu được state
của ứng dụng ToDo list là một object, với thuộc tính todoList
lưu trữ một mảng các ToDo item.
Bước tiếp theo, chúng ta cần xây dựng hàm reducer. Logic của hàm reducer phụ thuộc vào logic xử lý dữ liệu trong ứng dụng. Nguyên tắc cơ bản của một hàm reducer:
Chú ý
Với một ứng dụng Redux, có thể bao gồm nhiều hàm reducer. Mỗi hàm reducer thường xử lý một phần dữ liệu trong state. Ví dụ, reducer quản lý người dùng, reducer quản lý sản phẩm … Trong ví dụ ToDo, chúng ta chỉ sử dụng duy nhất một hàm reducer để quản lý danh sách ToDo item.
Trong ứng dụng ToDo, hàm reducer
có khả năng tạo ra một danh sách ToDo list mới, dựa trên danh sách ToDo list cũ và dữ liệu từ action (chứa thông tin về ToDo item vừa được submit từ người dùng):
Trong ví dụ trên, reducer
là một pure function, nói cách khác, không tạo ra side effect làm thay đổi state. Thay vào đó, reducer
tạo ra ra một state mới (Line 10–14) và trả về cho store
.
Đến đây, chúng ta đã hoàn tất các bước trong việc xây dựng một thư viện với những đặc điểm cơ bản giống như Redux. Trong phần tiếp theo, chúng ta quay trở lại với ứng dụng ToDo app để tìm cách tích hợp thư viện này.
Có nhiều cách để tích hợp store
vào ứng dụng ToDo list. Để đơn giản, ví dụ đưa ra lựa chọn tích hợp thông qua giá trị thuộc tính store
khi render container App
. Hãy xem xét source code của index.js
trong ví dụ sau:
Với thuộc tính store, container App
có hai nhiệm vụ (tham khảo source code dưới đây)
- Gửi thuộc tính store xuống component
TodoForm
, thông qua đóTodoForm
có thể dispatch actionADD_TODO
khi người dùng click vào submit button trên form (Line 42 & 46) - Đăng kí (subscribe) container
App
vớistore
để nhận thông báo khi giá trịstate
bên trongstore
thay đổi (Line 13). Cách thông báo này thể hiện ở việc gọi một hàm listener (subscribeStore
) cho App cung cấp (Line 30–38). Nhiệm vụ hàm này là cập nhật lại giá trị state của container App, thông qua đó, tác động đến thuộc tính props tasks của component TodoList, làm component này render lại với danh sách Todo item được cập nhật (Line 46)
Cuối cùng, trong component ToDoForm
, khi người dùng click vào submit button, chúng ta sẽ sử dụng action creator createAddTodo
, tạo ra một action và dispatch action đó đến store
. Đối tượng action, bao gồm nội dung của ToDo item được submit — taskName
, nhờ đó, hàm reducer
có thể tạo ra danh sách ToDo item mới.
Đến đây, chúng ta đã hoàn tất các bước để tích hợp Naive Redux, vào ứng dụng ToDo list. Một lần nữa, source code cuối cùng của chương trình có thể tham khảo tại đây: Github (branch: naive redux)
Bài tập
- Xây dựng ứng dụng ToDo với hàm reducer có khả năng thay đổi, xoá ToDo item.
Kết luận
Trong thế giới của JavaScript, có rất nhiều các thư viện khác nhau. Mỗi thư viện tập trung vào một vai trò cụ thể. React, tập trung việc xây dựng UI component. Redux, tập trung việc quản lý state của ứng dụng…Khi mới bắt đầu với JavaScript, ngay cả với những lập trình viên có kinh nghiệm (trong các ngôn ngữ khác), chúng ta rất dễ bị sock bởi vô vàn những thư viện này. Việc lựa chọn thư viện trong quá trình xây dựng ứng dụng JavaScript ngày nay là một công việc không dễ dàng (đã qua thời chúng ta chỉ biết jQuery). Tuy nhiên, hãy nhớ rằng, mọi thứ đều bắt đầu từ những nguyên tắc rất cơ bản trong lập trình. Nắm được điều này, sẽ giúp chúng ta vững vàng và sử dụng các thư viện này một cách tốt hơn.
Chúc các bạn một ngày vui vẻ. And happy coding. ~ Andy