Tự học ReactJS: React Patterns – Context module function

Bài toán

Giả sử chúng ta có đoạn code sử dụng React.ContextReact.useReducer cho ví dụ kinh điển Counter như sau:

// src/context/counter.js
const CounterContext = React.createContext()

function CounterProvider({step = 1, initialCount = 0, ...props}) {
  const [state, dispatch] = React.useReducer(
    (state, action) => {
      const change = action.step ?? step
      switch (action.type) {
        case 'increment': {
          return {...state, count: state.count + change}
        }
        case 'decrement': {
          return {...state, count: state.count - change}
        }
        default: {
          throw new Error(`Unhandled action type: ${action.type}`)
        }
      }
    },
    {count: initialCount},
  )

  const value = [state, dispatch]
  return <CounterContext.Provider value={value} {...props} />
}

function useCounter() {
  const context = React.useContext(CounterContext)
  if (context === undefined) {
    throw new Error(`useCounter must be used within a CounterProvider`)
  }
  return context
}

export {CounterProvider, useCounter}

// src/screens/counter.js
import {useCounter} from 'context/counter'

function Counter() {
  const [state, dispatch] = useCounter()
  const increment = () => dispatch({type: 'increment'})
  const decrement = () => dispatch({type: 'decrement'})
  return (
    <div>
      <div>Current Count: {state.count}</div>
      <button onClick={decrement}>-</button>
      <button onClick={increment}>+</button>
    </div>
  )
}

// src/index.js
import {CounterProvider} from 'context/counter'

function App() {
  return (
    <CounterProvider>
      <Counter />
    </CounterProvider>
  )
}

Nếu chú ý vào Counter component, chúng ta nhận thấy rằng có hai hàm increment decrement gọi dispatch. Có vẻ cách viết này hơi rườm rà và hơi bất tiện nếu chúng ta có gọi nhiều dispatch.

Cách 1. Sử dụng helper function

Có một cách là chúng ta tạo một hàm helper function trong userProvider và phải sử dụng kèm với React.useCallback, để sau này có thể thêm chúng vào dependency list nếu cần thiết.

const increment = React.useCallback(() => dispatch({type: 'increment'}), [
  dispatch,
])
const decrement = React.useCallback(() => dispatch({type: 'decrement'}), [
  dispatch,
])
const value = {state, increment, decrement}
return <CounterContext.Provider value={value} {...props} />

// now users can consume it like this:

const {state, increment, decrement} = useCounter()

Cách này cũng tương đối tốt nhưng có một cú pháp được Dan khuyên dùng hơn là sử dụng dispatch như params cho một hàm như sau:

Cách 2. Sử dụng context module function

// src/context/counter.js
const CounterContext = React.createContext()

function CounterProvider({step = 1, initialCount = 0, ...props}) {
  const [state, dispatch] = React.useReducer(
    (state, action) => {
      const change = action.step ?? step
      switch (action.type) {
        case 'increment': {
          return {...state, count: state.count + change}
        }
        case 'decrement': {
          return {...state, count: state.count - change}
        }
        default: {
          throw new Error(`Unhandled action type: ${action.type}`)
        }
      }
    },
    {count: initialCount},
  )

  const value = [state, dispatch]

  return <CounterContext.Provider value={value} {...props} />
}

function useCounter() {
  const context = React.useContext(CounterContext)
  if (context === undefined) {
    throw new Error(`useCounter must be used within a CounterProvider`)
  }
  return context
}


const increment = dispatch => dispatch({type: 'increment'})
const decrement = dispatch => dispatch({type: 'decrement'})

export {CounterProvider, useCounter, increment, decrement}

// src/screens/counter.js
import {useCounter, increment, decrement} from 'context/counter'

function Counter() {
  const [state, dispatch] = useCounter()
  return (
    <div>
      <div>Current Count: {state.count}</div>
      <button onClick={() => decrement(dispatch)}>-</button>
      <button onClick={() => increment(dispatch)}>+</button>
    </div>
  )
}

Ví dụ

Giả sử bạn có hàm handleSubmit như sau:

function handleSubmit(event) {
    event.preventDefault()
    // 🐨 move the following logic to the `updateUser` function you create above
    userDispatch({type: 'start update', updates: formState})
    userClient.updateUser(user, formState).then(
      updatedUser => userDispatch({type: 'finish update', updatedUser}),
      error => userDispatch({type: 'fail update', error}),
    )
  }

Chúng ta có thể sử dụng pattern này để tạo một hàm cho việc xử lí updateUser như sau:

async function updateUser(dispatch, user, updates) {
  dispatch({type: 'start update', updates})
  try {
    const updatedUser = await userClient.updateUser(user, updates)
    dispatch({type: 'finish update', updatedUser})
    return updatedUser
  } catch (error) {
    dispatch({type: 'fail update', error})
    return Promise.reject(error)
  }
}

function handleSubmit(event) {
    event.preventDefault()
    updateUser(userDispatch, user, formState).catch(() => {
      /* ignore the error */
    })
  }

Hàm này nằm bên ngoài component và nhận vào dispatch. Nó cũng có thể dễ dàng tái sử dụng trong trường hợp cần.

Ưu điểm

  • Code gọn gàng hơn và có thể tách riêng ra file khác nếu hàm này xử lý nhiều việc hơn
  • Tránh được vấn đề dependecy list sau này vì decrementincrement được khai báo bên ngoài component
  • Chúng luôn không thay đổi khi component re-render và chúng ta cũng không cần sử dụng React.useCallback như cách đầu tiên

Nguồn: Epic React by Kent C.Dodds

Leave a Reply

Your email address will not be published. Required fields are marked *