React cho ra con hàng React Hook làm các anh em thư viện liên quan cũng phải chạy theo và cho ra phiên bản hook riêng, ông Redux cũng không nằm ngoài cuộc chơi này .
Redux có thể nói là một thư viện quản lý state phổ biến nhất cho React (mình chỉ nói React thôi nhé, không mấy ông vào bắt bẻ Redux dùng cho mọi framework mà bla bla ? ). Theo như anh em code trước đây thì ta có connect()
– một Higher Order Component (HOC) giúp chúng ta nhận state và dispatch action từ store tại component. Gần đây chúng ta có thêm một số hook mới, căn bản là những API mới cho phép chúng ta subcribe Redux store và dispatch các action mà không cần phải bao gói component vào trong connect()
.
Tụi nó là ?
- useSelector
- useDispatch
- useStore (cái này hôm nay sẽ không bàn tới, vì theo mình khá ít dùng)
Trong bài này chúng ta sẽ xây dựng một shop sản phẩm siêu đơn giản sử dụng cả 2 làconnect()
HOC truyền thống và Redux hook mới.
Mục đích bài viết này sẽ cho bạn thấy được
- Cách kết nối React component với store bằng
connect()
- Cách kết nói React component với store bằng Redux hook mới
- Ưu nhược của Redux hook
Link codesandbox mình sẽ để cuối bài, bạn có thể test bằng cách log component.
Cấu trúc thư mục
Phần cài đặt package thì mình chỉ cài thêm redux và react-redux thôi nhé.
Cấu hình Redux store
File store/store.js
1 2 3 4 |
import { createStore } from 'redux' import { rootReducer } from './reducer' export const store = createStore(rootReducer) |
Tiếp theo là store/reducer.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
import * as types from './constants' const initialState = { isOpen: true, products: [ { id: '1a', name: 'Macbook Pro', quantity: 3 }, { id: '2b', name: 'Iphone X', quantity: 6 }, { id: '3c', name: 'Apple Watch', quantity: 4 } ] } export const rootReducer = (state = initialState, action) => { switch (action.type) { case types.ADD_TO_CART: return { ...state, products: state.products.map((product) => product.id === action.payload.id ? { ...product, quantity: product.quantity - 1 } : product ) } case types.TOGGLE_OPEN_SHOP: return { ...state, isOpen: !state.isOpen } default: return state } } |
Tiếp theo store/constants.js
1 2 3 |
export const ADD_TO_CART = 'ADD_TO_CART' export const TOGGLE_OPEN_SHOP = 'TOGGLE_OPEN_SHOP' |
Và cuối cùng store/actions.js
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import * as types from './constants' export const addToCart = (product) => { return { type: types.ADD_TO_CART, payload: product } } export const toggleOpenShop = () => { return { type: types.TOGGLE_OPEN_SHOP } } |
Cấu hình components
Hehe , đây rồi, chúng ta sẽ cần tạo file ProductList.js
và file ProductListHook.js
để render file ProductItem.js
. Chúng ta chỉ có 3 component đơn giản vậy thôi.
App.js
sẽ là component lớn nhất render cả 2 ProductList.js
và ProductListHook.js
App.js
dùng connect()
để lấy state và dispatch action
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import React from 'react' import { connect } from 'react-redux' import ProductList from './components/ProductList' import { toggleOpenShop } from './store/actions' import ProductListHook from './components/ProductListHook' function App(props) { const { isOpen, toggleOpenShop } = props return ( <> <div className='shop-status'> <h1>{isOpen ? 'OPEN' : 'CLOSE'}</h1> <button onClick={toggleOpenShop}> {isOpen ? 'open' : 'close'} shop </button> </div> <ProductList /> <ProductListHook /> </> ) } const mapState = (state) => ({ isOpen: state.isOpen }) const mapDispatch = { toggleOpenShop } export default connect(mapState, mapDispatch)(App) |
file components/ProductList.js
đại diện cho việc dùng connect()
HOC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import React from 'react' import ProductItem from './ProductItem' import { connect } from 'react-redux' import { addToCart } from '../store/actions' function ProductList({ productList, addToCart }) { return ( <> <h2 className="title">ProductList use connect Redux</h2> <div className='product-list'> {productList.map((productItem) => ( <ProductItem key={productItem.id} productItem={productItem} addToCart={addToCart} /> ))} </div> </> ) } const mapState = (state) => ({ productList: state.products }) const mapDispatch = { addToCart } export default connect(mapState, mapDispatch)(ProductList) |
file components/ProductListHook.js
đại diện cho dùng Redux hook mới, cụ thể là useSelector
và useDispatch
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import React from 'react' import ProductItem from './ProductItem' import { useSelector, useDispatch } from 'react-redux' import { addToCart } from '../store/actions' export default function ProductListHook() { const productList = useSelector((state) => state.products) const dispatch = useDispatch() const dispatchAddToCart = (product) => dispatch(addToCart(product)) return ( <> <h2 className="title">ProductList use hook Redux</h2> <div className='product-list'> {productList.map((productItem) => ( <ProductItem key={productItem.id} productItem={productItem} addToCart={dispatchAddToCart} /> ))} </div> </> ) } |
Và cuối cùng là file component/ProductItem.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import React from 'react' function ProductItem({ productItem, addToCart }) { return ( <div className='product-item'> <div className='product-item-title'>{productItem.name}</div> <div className='product-item-quantity'> <span>x{productItem.quantity}</span> <button onClick={() => addToCart(productItem)} disabled={productItem.quantity === 0} > Mua sản phẩm </button> </div> </div> ) } export default ProductItem |
Và kết quả sẽ như thế này đây
Ohhhh, có một chút khác biệt về cách dùng 2 hook mới là useSelector và useDispatch. Trước khi đi đến thử nghiệm thì hãy đọc lại một chút thông tin về chúng nhé.
useSelector là gì
Hook này cho phép chúng ta lấy state từ** Redux store** bằng cách sử dụng một selector function làm tham số đầu vào. Trong đoạn code phía trên bạn thấy thì mình có trả về mảng products từ store.
Mặc dù nó thực hiện công việc như mapStateToProps
( ở trên mình viết mapState cho gọn) nhưng nó vẫn có một số khác biệt mà bạn cần phải quan tâm.
- mapStateToProps chỉ return về 1 object, còn useSelector có thể return bất cứ giá trị nào
- Khi dispatch một action,
useSelector
sẽ thực hiện so sánh tham chiếu với giá trị được return trước đó và giá trị hiện tại. Nếu chúng khác nhau, component sẽ bị re-render. Nếu chúng giống nhau, component sẽ không re-render.
Nếu các bạn chưa biết thìmapState
là một function sẽ luôn được chạy lại mỗi khi store có một sự thay đổi bất kì nào trong đó. VớimapState
, tất cả các trường được return lại thành một dạng object kết hợp. Vậy nên mỗi khimapState
chạy thì nó sẽ return về một object với tham chiếu mới. Hàmconnect()
sẽ thực hiện so sánh nông với object màmapState
trả về, nếu khác nhau thì sẽ re-render lại component. Tức hiểu cặn kẽ hơn là so sánh tham chiếu (so sánh ===) các trường bên trong object mà mapState trả về, chỉ cần 1 trường khác nhau là sẽ bị coi là khác nhau. ?
Suy nghĩ kĩ một chút nhé. Thoạt nhìn cách so sánh useSelector
vs connect()
có khác nhau 1 tẹo nhưng nếu ta khai báo nhiều useSelector
cho mỗi state khác nhau thay vì gom lại một cục object duy nhất thì cách so sánh lại tương đương với connect()
.
Ồ, vậy là có vẻ như 2 bên tương đương rồi ha, test thử bài test nào. Mình sẽ click liên tục 5 lần vào button open shop và đây là kết quả nhận được khi component render.
Ohhh, Vậy nguyên nhân do đâu ? ?
Trong ngữ cảnh bài viết này thì mình sẽ phân tích như sau
- Yếu tố ảnh hưởng đến việc re-render
ProductList
chỉ có những state màmapState
đăng kí. Dù choApp
bị re-render 5 lần do stateisOpen
thay đổi, nhưng component conProductList
được bao bọc bởiconnect()
HOC, nó sẽ so sánh nông các state để quyết định có re-render hay không (cách này hoạt động tương tự nhưReact.memo
). Nếu anh em để ý trên hình Profiler thì ProductList được bao bởi một component làConnectFuntion (Memo) - Còn với
ProductListHook
thì có 2 yếu tố ảnh hưởng đến việc re-render đó là component chaApp
và state màuseSelector
đăng kí. Vì vậy dù cho stateproducts
không thay đổi nhưng component cha re-render 5 lần dẫn đếnProductListHook
cũng bị re-render 5 lần.
Để khắc phục điều này thì anh em có thể dùng một HOC là React.memo()
cho ProductListHook
.
useDispatch là gì
Hook này đơn giản chỉ là return về một tham chiếu đến dispatch function từ Redux store và được sử dụng để dispatch các action. Nhưng sẽ có vài điều mà mình cần cho các bạn biết.
file components/ProductListHook.js
sau khi thêm React.memo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import React from 'react' import ProductItem from './ProductItem' import { useSelector, useDispatch } from 'react-redux' import { addToCart } from '../store/actions' function ProductListHook() { const productList = useSelector((state) => state.products) const dispatch = useDispatch() const dispatchAddToCart = (product) => dispatch(addToCart(product)) return ( <> <h2 className='title'>ProductList use hook Redux</h2> <div className='product-list'> {productList.map((productItem) => ( <ProductItem key={productItem.id} productItem={productItem} addToCart={dispatchAddToCart} /> ))} </div> </> ) } export default React.memo(ProductListHook) |
Nếu các bạn nhìn ProductListHook.js
component thì có thể thấy rằng mình truyền một anonymous function là dispatchAddToCart
xuống cho ProductItem
component.
Hãy xem điều gì sẽ xảy ra khi mình click một lần vào nút Mua sản phẩm của ProductList.
Làm điều tương tự với ProductListHook xem thử kết quả như thế nào
Tôi chỉ thay đổi 1 sản phẩm, tôi chỉ muốn sản phấm đó re-render thôi, nhưng ở đây lại bị re-render hẳn 3 sản phẩm!
Nếu phân tích kĩ thì
- Bên
ProductList
,ProductListItem
đầu tiên re-render bởi props thay đổi (productItem
), 2 cái còn lại là do component cha re-render - Bên
ProductListHook
,ProductListItem
đầu tiên re-render bởi props thay đổi (productItem
,addToCart
), 2 cái còn lại là do props (addToCart
).
À, Vậy biết được nguyên nhân rồi, vậy cùng để khắc phục choProductList
thì chúng ta chỉ cần dùngReact.memo
bao ngoàiProductItem
là được.
file components/ProductItem.js
lúc này
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import React from 'react' function ProductItem({ productItem, addToCart }) { return ( <div className='product-item'> <div className='product-item-title'>{productItem.name}</div> <div className='product-item-quantity'> <span>x{productItem.quantity}</span> <button onClick={() => addToCart(productItem)} disabled={productItem.quantity === 0} > Mua sản phẩm </button> </div> </div> ) } export default React.memo(ProductItem) |
Còn với bên ProductListHook
thì sẽ phức tạp hơn, chúng ta phải đi giải quyết thêm prop addToCart
. Vì vậy cần làm 2 việc đó là dùng React.memo
cho ProductItem
và useCallback
cho anonymous function là dispatchAddToCart
file components/ProductListHook.js
lúc này
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import React, { useCallback } from 'react' import ProductItem from './ProductItem' import { useSelector, useDispatch } from 'react-redux' import { addToCart } from '../store/actions' function ProductListHook() { const productList = useSelector((state) => state.products) const dispatch = useDispatch() const dispatchAddToCart = useCallback((product) => dispatch(addToCart(product)), [dispatch]) return ( <> <h2 className='title'>ProductList use hook Redux</h2> <div className='product-list'> {productList.map((productItem) => ( <ProductItem key={productItem.id} productItem={productItem} addToCart={dispatchAddToCart} /> ))} </div> </> ) } export default React.memo(ProductListHook) |
Và ta đã có kết quả tương tự với ProductList phía trên, nhưng tốn thêm 1 công đoạn nữa.
Vậy đi tới kết luận được rồi :
Ưu nhược của việc sử dụng Redux Hooks
Ưu điểm
Không còn connect()
HOC => ít node trong hệ thống component hơn.
Nhược điểm
Bạn sẽ mất tính năng tự động memo mà connect()
cung cấp.
Thoạt nhìn cứ tưởng đơn giản, nhưng cuối cùng lại dài dòng hơn ?
Vậy tôi nên có nên dùng Redux Hook?
À, cái này tùy thuộc vào bạn thôi. Nếu bỏ connect()
, bạn sẽ mất nhiều tính năng tối ưu performance mà nó cung cấp. Điều này nghĩa là bạn phải để ý hơn đến việc re-render component vốn đã là vấn đề đau đầu của React. Hiện tại cá nhân mình không nghĩ rằng mình sẽ chuyển sang dùng Redux Hook.
Lời khuyên của mình khi các bạn bắt đầu sử dụng Redux Hook hãy tự hỏi
- Các hook này có thực sự tốt hơn cách hiện tại hay không?
- Tôi sẽ đánh mất điều gì khi sử dụng hook này?
Luôn nhớ rằng Redux hook chỉ là tùy chọn, một phương thức thêm vào thôi! Bạn không bắt buộc phải chuyển qua dùng chúng.
Cảm mọi người đã đọc đến đây. Hi vọng bài viết có ích với mọi người