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 :mrgreen: .

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 statedispatch 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à 🙄

  1. useSelector
  2. useDispatch
  3. 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

  1. Cách kết nối React component với store bằng connect()
  2. Cách kết nói React component với store bằng Redux hook mới
  3. Ư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 reduxreact-redux thôi nhé.

Cấu trúc thư mục react redux

Cấu hình Redux store

File store/store.js

import { createStore } from 'redux'
import { rootReducer } from './reducer'
export const store = createStore(rootReducer)

Tiếp theo là store/reducer.js

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

export const ADD_TO_CART = 'ADD_TO_CART'
export const TOGGLE_OPEN_SHOP = 'TOGGLE_OPEN_SHOP'

Và cuối cùng store/actions.js

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 :mrgreen: , đâ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.jsProductListHook.js

App.js dùng connect() để lấy statedispatch action

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

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à useSelectoruseDispatch

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

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

UX/UI sau khi hoàn thành

UX/UI sau khi hoàn thành

Ohhhh, có một chút khác biệt về cách dùng 2 hook mới là useSelectoruseDispatch. 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.

  1. mapStateToProps chỉ return về 1 object, còn useSelector có thể return bất cứ giá trị nào
  2. 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ới mapState, 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 khi mapState chạy thì nó sẽ return về một object với tham chiếu mới. Hàm connect() 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. 🙄

Anh em nào chưa hiểu về so sánh tham chiếu, so sánh nông thì có thể đọc bài này. Nếu muốn hiểu rõ hơn về vùng nhớ tham chiếu thì đọc bài này của mình

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.

Flamegraph profiler react

ProductList dùng mapStateToProps không bị re-render

Flamegraph profiler react

ProductListHook dùng useSelector bị re-render 5 lần (anh em đừng để ý dòng “why did this render? Hook changed” nhé, đây là lỗi của Profiler nó phát hiện không chính xác, mình sẽ giải thích rõ bên dưới)

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 statemapState đăng kí. Dù cho App bị re-render 5 lần do state isOpen thay đổi, nhưng component con ProductList được bao bọc bởi connect() 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 cha AppstateuseSelector đăng kí. Vì vậy dù cho state products không thay đổi nhưng component cha re-render 5 lần dẫn đến ProductListHook 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.

Flamegraph profiler react

Kết quả ProductListHook kết hợp React.memo không bị re-render

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

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.

3 component ProductItem mỗi cái re-render 1 lần

3 component ProductItem mỗi cái re-render 1 lần

Làm điều tương tự với ProductListHook xem thử kết quả như thế nào

3 component ProductItem, mỗi cái đều re-render 1 lần

3 component ProductItem, mỗi cái đều re-render 1 lần

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 cho ProductList thì chúng ta chỉ cần dùng React.memo bao ngoài ProductItem là được.

file components/ProductItem.js lúc này

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)
Kết quả chỉ có 1 ProductItem re-render

Kết quả chỉ có 1 ProductItem re-render

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 ProductItemuseCallback cho anonymous function là dispatchAddToCart

file components/ProductListHook.js lúc này

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.

Kết quả chỉ có 1 ProductItem chỉ re-render

Kết quả chỉ có 1 ProductItem chỉ re-render

Vậy đi tới kết luận được rồi :mrgreen:

Ư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, nếu thấy hay có thể comment động viên mình và nhớ like fanpage để nhận thông báo cho các bài viết chất lượng hơn nhé 😆

Click vào đây nếu trình duyệt không hiển thị codesanbox

Tham khảo
How Redux Connect compares to the new Redux Hooks.