1. Lời mở đầu
Bài viết này blog sẽ giới thiệu Redux, một thư viện JavaScript được sử dụng để quản lý trạng thái (state) của ứng dụng, thường kết hợp với các framework JavaScript như React. Cơ chế hoạt động của Redux có thể được tóm tắt bằng một sơ đồ đơn giản như sau:
…Ủa không đơn giản hả?
Đúng vậy, blog cũng đã trải qua điều này. “Action, reducer, dispatch”??? Chưa bao giờ blog hiểu những thuật ngữ này cho đến khi thực sự triển khai chúng trong dự án của mình. Đó là lý do tôi viết bài này, một hướng dẫn về React Redux dành cho người mới bắt đầu, bắt đầu từ việc nghiên cứu vấn đề mà nó giải quyết, và sau đó là cách triển khai. Blog tin rằng chỉ cần 15 phút đọc kỹ bài viết, bạn sẽ hiểu được sơ đồ ở trên và có khả năng sử dụng Redux ngay trong dự án của bạn một cách hiệu quả.
(Bài viết mặc định người đọc đã có kiến thức cơ bản về React, state, props nên nếu bạn chưa biết thì nhớ tìm hiểu trước khi bắt đầu nhé)
2. Vấn đề và giải pháp
2.1. Vấn đề
Hãy tưởng tượng bạn đang phát triển một ứng dụng quản lý phim với khả năng đăng nhập và xem danh sách các bộ phim. Các component được tổ chức như sau:
- MoviesList: Hiển thị danh sách các phim, bao gồm 1 list các component Movie
- Movie: là 1 item trong MoviesList, hiển thị thông tin một phim
- Login: chức năng đăng nhập
Chúng ta có một danh sách thông tin về các bộ phim, và muốn biết là làm thế nào dữ liệu được truyền giữa các thành phần trong ứng dụng. Theo kiến thức cơ bản đã học, chúng ta có thể sử dụng khái niệm “state” để giữ dữ liệu trong thành phần MoviesList. Sau đó, chúng ta có thể truyền dữ liệu này xuống thành phần Movie dưới dạng “props”.
Cách này ổn cho đến khi ta thêm 1 component mới, ví dụ như Search, để search các phim, và nó cũng sử dụng data. Vì là 1 component riêng, ta không thể truyền data từ component MovieList sang bằng props được:
Hiện tại, để Search component có thể nhận dữ liệu, chúng ta phải truyền dữ liệu lên từ component ở cấp cao hơn, thường là App component. Nhìn chung, theo mô hình này, khi ứng dụng mở rộng với nhiều loại dữ liệu khác nhau, tất cả đều phải được chuyển qua App component. Hơn nữa, các hàm xử lý dữ liệu cũng phải được đặt ở App component, dẫn đến việc App trở nên rất lớn và chịu trách nhiệm vô vàn công việc. Điều này làm cho thiết kế trở nên không hiệu quả!
2.2. Giải pháp
Trong Redux, chúng ta tổ chức tất cả dữ liệu và trạng thái của ứng dụng vào một nơi gọi là “store”. Khi một component cần truy cập hoặc thay đổi dữ liệu, nó sẽ thực hiện hành động này trên store. Điều này giúp đảm bảo rằng tất cả các components trong ứng dụng sử dụng và duy trì cùng một tập hợp dữ liệu, vì store là một không gian toàn cục mà toàn bộ ứng dụng chia sẻ.
Ý tưởng chính là như vậy, tiếp theo ta cùng tìm hiểu về cấu trúc và cách sử dụng Redux được mô tả trong một ví dụ đơn giản: ứng dụng tăng/giảm 1 biến đếm counter.
3. Sử dụng Redux
3.1. Setup môi trường
Đầu tiên để sử dụng Redux trong project React, ta bật terminal trong thư mục project, cài đặt 2 thư viện redux và react-redux:
npm install redux react-redux
3.2. Cách hoạt động của Redux
Các thành phần của Redux bao gồm:
- Store: Store đơn giản là 1 object chứa tất cả state toàn cục của ứng dụng. Nhưng thay vì lưu các state, nó lưu các reducer, sẽ được nói sau.
- Các Action: Khi ta định nghĩa các action, ta khai báo các tên của hành động trong ứng dụng. Lấy ví dụ ta có 1 state là counter và cần 2 phương thức để tăng và giảm giá trị của counter. Lúc này ta định nghĩa 2 action có tên là ‘INCREMENT‘ và ‘DECREMENT‘ và chỉ vậy thôi, việc xử lý thay đổi state của counter sẽ nhường cho reducer.
- Các Reducer: 1 reducer tương đương với 1 state nhưng kèm theo các mô tả state sẽ thay đổi như thế nào khi các action khác nhau được gọi. Trong ví dụ ta có reducer là counter, nó lưu state của counter và kiểm tra action vừa được gọi là INCREMENT hay DECREMENT và trả về state mới là state+1 hay state-1 tương ứng.
- Các Dispatch: Khi cần dùng 1 action ở component, ta gọi action đó đơn giản bằng cách sử dụng phương thức dispatch. VD: dispatch(increment()), dispatch(decrement()).
Sơ đồ minh họa:
Giờ xem code nữa là hiểu luôn nè. Tạo action là dễ nhất nên ta sẽ tạo action:
// ACTIONS const increment = () => { return { type: "INCREMENT", }; }; const decrement = () => { return { type: "DECREMENT", }; };
Tiếp theo là tạo counter reducer. Nó nhận vào 2 tham số là state và action, trả về state mới tùy theo action được gọi. Ta mặc định state ban đầu của counter là 0, code như sau:
// REDUCER const counter = (state = 0, action) => { switch (action.type) { case "INCREMENT": return state + 1; case "DECREMENT": return state - 1; } };
Có counter reducer rồi, ta bỏ nó vào store. Redux hỗ trợ phương thức createStore nhận vào reducer và trả về store:
// STORE import { createStore } from "redux"; let store = createStore(counter);
Khi cần dùng action, ta gọi dispatch từ store và truyền vào action.
// DISPATCH store.dispatch(increment()); store.dispatch(decrement()); store.dispatch(decrement()); // counter state result: -1
Vậy là xong, bỏ hết code vào index.js để bạn có cái nhìn tổng quát:
import React from "react"; import ReactDOM from "react-dom"; import { createStore } from "redux"; import App from "./App"; // ACTIONS const increment = () => { return { type: "INCREMENT", }; }; const decrement = () => { return { type: "DECREMENT", }; }; // REDUCER const counter = (state = 0, action) => { switch (action.type) { case "INCREMENT": return state + 1; case "DECREMENT": return state - 1; } }; // STORE import { createStore } from "redux"; let store = createStore(counter); // DISPATCH store.dispatch(increment()); store.dispatch(decrement()); store.dispatch(decrement()); // counter state result: -1 ReactDOM.render(<App />, document.getElementById("root"));
3.3. Tổ chức Redux trong project
Có nhiều cách để tổ chức project sử dụng redux, với người mới bắt đầu, mình sẽ lưu tất cả action và reducer trong 2 thư mục riêng:
actions/counter.js
Đầu tiên ta định nghĩa tất cả counter action. Để ý mình vừa thêm parameter number cho các action để có thể tăng/giảm một giá trị theo ý muốn. Các action lúc này ngoài tên của nó ra (type), nó còn mang theo data là number (data đi kèm này thường được gọi là payload).
export const increment = (number) => { return { type: "INCREMENT", payload: number, }; }; export const decrement = (number) => { return { type: "DECREMENT", payload: number, }; };
reducers/counter.js
Tiếp theo định nghĩa counter reducer. Thay vì cộng trừ 1, ta sẽ cộng trừ payload đi kèm với action như đã nói ở trên. Đơn giản mà đúng không? Nhớ thêm case default để trả về chính state đó khi không có action tương ứng nhé.
export const counterReducer = (state = 0, action) => { switch (action.type) { case "INCREMENT": return state + action.payload; case "DECREMENT": return state - action.payload; default: return state; } }; export default counterReducer;
reducers/index.js
Thông thường, ứng dụng sẽ có nhiều reducer nên bạn phải gộp tất cả reducer lại để bỏ vào trong store. Mình sử dụng hàm combineReducer của redux để hợp nhất tất cả reducer thành 1 reducer là allReducers.
import { combineReducers } from "redux"; import counter from "./counter"; const allReducers = combineReducers({ counter, // add more reducers here });
index.js
Ta sử dụng hàm createStore để tạo store chứa allReducers. Tiếp theo ta gói <App/> bên trong 1 component hỗ trợ của react-redux là Provider, nhờ đó tất cả component trong <App/> có thể truy cập được store.
import React from "react"; import ReactDOM from "react-dom"; import { createStore } from "redux"; import { Provider } from "react-redux"; import App from "./App"; import allReducers from "./reducers"; const store = createStore(allReducers); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById("root") );
App.js
Cùng test kết quả bằng cách hiện giá trị counter cùng 2 nút tăng giảm, mỗi lần ấn vào counter tăng/giảm 5 đơn vị, ta sẽ:
- Sử dụng useSelector của react-redux để lấy state counter từ store.
- Sử dụng useDispatch để trả về function dispatch, truyền increment và decrement vào dispatch để gọi 2 action này.
import React from "react"; import { useSelector, useDispatch } from "react-redux"; import { increment, decrement } from "./actions/counter"; function App() { const counter = useSelector((state) => state.counter); const dispatch = useDispatch(); return ( <div> <h1>Counter {counter}</h1> <button onClick={() => dispatch(increment(5))}>Increment</button> <button onClick={() => dispatch(decrement(5))}>Decrement</button> </div> ); } export default App;
Rồi bây giờ nhìn lại bức hình hồi nãy nhé:
4. Tổng kết
“People often choose Redux before they need it” – Dan Abramov, You Might Not Need Redux
Bạn nên dùng Redux vì:
- Project càng lớn, giá trị redux càng nhiều, nhất là khi app có nhiều shared state và việc xử lý state phức tạp, được handle ở nhiều nơi. Redux là lựa chọn tốt nhất để quản lý state trong project lớn nếu bạn sử dụng React.
- Phân chia rõ ràng giữa shared state (các state toàn cục, app data) và UI state (thường nằm cục bộ trong 1 component).
- Để đi xin việc, chém gió với đồng nghiệp bằng những thuật ngữ siêu ảo: reducer, dispatch, thunk.
Bạn không nên dùng Redux vì:
- Code rất nhiều để làm được rất ít chức năng
- Nếu bạn chỉ cần xử lý state phức tạp: Sử dụng useReducer hook
- Nếu bạn chỉ cần xử lý state global: React Context
- Việc dò tìm action để dispatch (linear search, O(n)) sẽ ảnh hưởng đến performance so với cách tương tác với state trực tiếp
Tóm lại, Redux có nhiều hữu ích đi kèm 1 số side effects, như một loại thuốc, hãy đọc kỹ HDSD trước khi dùng.
Nhân tiện, nói về side effects, với Redux ta không dùng side effects trực tiếp trong việc xử lý state, mà phải dùng middlewares như redux thunk, redux saga. Mọi người có thể tìm hiểu thêm về chủ đề này trong doc của redux: https://redux.js.org/tutorials/fundamentals/part-6-async-logic
Peace.
5. Nguồn tham khảo
- Redux – A Predictable State Container for JS Apps – https://redux.js.org/
- React Redux – Official React bindings for Redux – https://react-redux.js.org/
- Dev Ed – Redux For Beginners – https://www.youtube.com/watch?v=CVpUuw9XSjY
One thought on “Học React Redux trong 15 phút”