1. Lời mở đầu
Redux là 1 thư viện Javascript để quản lý state của ứng dụng, thường được sử dụng với React. Việc tìm hiểu Redux nhìn chung khá dông dài, vì bạn sẽ gặp một loạt các thuật ngữ mới như dispatch, reducer, combineReducers, store, v.v., cùng flow khá phức tạp của nó:
Đó là lý do mình viết bài này, một hướng dẫn React Redux cho người mới bắt đầu với mục tiêu giúp bạn hiểu được vấn đề Redux giải quyết và cách sử dụng trong project thực tế. Với nội dung được cô đọng nhất có thể, tập trung vào phần quan trọng, nói nhanh phần râu ria, sử dụng các ví dụ điển hình thay vì ẩn dụ cuộc sống, sơ đồ bla bla, nhưng vẫn đảm bảo dễ hiểu và chính xác. Mình tin rằng chỉ cần 15 phút đọc kỹ bài viết, bạn cũng đủ hiểu được tinh thần của Redux hoặc tốt hơn là có thể sử dụng Redux ngay trong project của bạn.
(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 đề
Tưởng tượng bạn đang viết một ứng dụng quản lý phim có tính năng đăng nhập và xem danh sách các 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
Ta có data là danh sách thông tin các phim, khi đó data được chuyển đổi qua lại giữa các component trong ứng dụng như thế nào? Theo kiến thức cơ bản đã được học, ta có thể để data là state trong MoviesList, rồi truyền data xuống component 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:
Lúc này ta buộc phải đưa data lên component ở trên nữa là App mới có thể truyền data xuống Search component. Dễ thấy theo mô hình này, khi ứng dụng mở rộng thêm các loại data khác, tất cả sẽ được đưa vào App và các hàm xử lý data cũng phải định nghĩa ở App, khiến App component trở nên khổng lồ với vô vàn trách nhiệm. Bad design!
2.2. Giải pháp
Với Redux, ta đưa tất cả data, các state vào 1 nơi gọi là store, khi component nào cần dùng hoặc thay đổi data, nó sẽ lấy hoặc cập nhật data ở store. Các data trong các component là thống nhất với nhau vì store là toàn cục trong toàn bộ App.
Ý 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. Nó lưu các state đó dưới dạng phức tạp hơn là 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"));
Trong thực tế, ta cần tổ chức Redux một cách hệ thống hơn thay vì quăng tất cả vào index.js, ta đi đến phần tiếp theo.
3.3. Tổ chức Redux trong project
Lưu tất cả action và reducer trong 2 thư mục riêng:
actions/counter.js
export const increment = (number) => { return { type: "INCREMENT", payload: number, };
};
export const decrement = (number) => { return { type: "DECREMENT", payload: number, };
};
Đầ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 (payload là thuật ngữ thường được dùng thay cho data).
reducers/counter.js
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;
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é.
reducers/index.js
import { combineReducers } from "redux"; import counter from "./counter"; const allReducers = combineReducers({ counter, // add more reducers here
});
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.
index.js
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")
);
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.
App.js
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;
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, cú pháp như trên.
- Sử dụng useDispatch để trả về function dispatch, truyền increment và decrement vào dispatch để gọi 2 action này.
4. Tổng kết
"People often choose Redux before they need it" - Dan Abramov, You Might Not Need Redux
Thật ra mình không phải big fan của Redux, cùng là state managment library nhưng Vuex của Vue cung cấp 1 giải pháp dễ dàng hơn, bớt rối hơn. Với Redux, bạn tạo rất nhiều file, code rất nhiều dòng để tạo ra rất ít chức năng, nên nó không worth nếu ứng dụng của bạn không cần quản lý quá nhiều globlized state. Đôi khi với project nhỏ bạn vẫn có thể sống được với React Context và cách chuyển state xuống props như đã nói ở ví dụ đầu bài.
Nhưng đừng quên những lý do để sử dụng Redux:
- Đối với các project React lớn, Redux là lựa chọn tốt nhất để quản lý các app state
- Phân chia rõ ràng giữa app state (các state toàn cục, app data) và UI state (thường nằm cục bộ trong 1 component)
- Thường được yêu cầu đi kèm với React trong các job phát triển web
Nói tóm lại, Redux mang lại nhiều hữu ích, nhưng cũng có nhiều trade off. Khi bạn đánh đổi một thứ gì đó, hãy đảm bảo rằng bạn thu về phần có lợi hơn.
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