Tự học ReactJS: React Patterns – Sử dụng control props

Thỉnh thoảng, người dùng (dev) muốn điều khiển và quản lí những state bên trong một component từ bên ngoài. Pattern state reducer cho phép chúng ta quản lí state bên trong component Toggle hiệu quả. Nhưng bây giờ mục tiêu là phải cho phép người dùng (dev) tự quản lí state (on/off) của Toggle và truyền từ bên ngoài vào.

Đây là một khái niệm đã quá quen từ React, được gọi là “Controller Components

function MyCapitalizedInput() {
  const [capitalizedValue, setCapitalizedValue] = React.useState('')

  return (
    <input
      value={capitalizedValue}
      onChange={e => setCapitalizedValue(e.target.value.toUpperCase())}
    />
  )
}

Với component <MyCapitalizedInput /> bạn thấy giá trị value của input được truyền vào và điều khiển bởi hàm setCapitalizedValue. Ở đây người dùng có thể tự do truyền vào giá trị input mà họ muốn. Và họ quản lí state đó. Đây được gọi là pattern control props. Bởi vì value là một props của input và chúng ta control props đó.

Một ví dụ control props khác:

function MyTwoInputs() {
  const [capitalizedValue, setCapitalizedValue] = React.useState('')
  const [lowerCasedValue, setLowerCasedValue] = React.useState('')

  function handleInputChange(e) {
    setCapitalizedValue(e.target.value.toUpperCase())
    setLowerCasedValue(e.target.value.toLowerCase())
  }

  return (
    <>
      <input value={capitalizedValue} onChange={handleInputChange} />
      <input value={lowerCasedValue} onChange={handleInputChange} />
    </>
  )
}

Một số thư viện sử dụng pattern control props

Control props cho Toggle

Trong bài tập này, chúng ta sẽ tạo một component <Toggle /> nhận vào props ononChange. Hai props này hoạt động giống như valueonChange trong component <input />. Bạn sẽ cho phép người dùng điều khiển giá trị on bằng hàm onChange được truyền bên ngoài vào.

import * as React from 'react'
import {Switch} from '../switch'

const callAll = (...fns) => (...args) => fns.forEach(fn => fn?.(...args))

const actionTypes = {
  toggle: 'toggle',
  reset: 'reset',
}

function toggleReducer(state, {type, initialState}) {
  switch (type) {
    case actionTypes.toggle: {
      return {on: !state.on}
    }
    case actionTypes.reset: {
      return initialState
    }
    default: {
      throw new Error(`Unsupported type: ${type}`)
    }
  }
}

function useToggle({
  initialOn = false,
  reducer = toggleReducer,
  onChange,
  on: controlledOn,
} = {}) {
  const {current: initialState} = React.useRef({on: initialOn})
  const [state, dispatch] = React.useReducer(reducer, initialState)
  const onIsControlled = controlledOn != null
  const on = onIsControlled ? controlledOn : state.on

  function dispatchWithOnChange(action) {
    if (!onIsControlled) {
      dispatch(action)
    }
    onChange?.(reducer({...state, on}, action), action)
  }

  const toggle = () => dispatchWithOnChange({type: actionTypes.toggle})
  const reset = () =>
    dispatchWithOnChange({type: actionTypes.reset, initialState})

  function getTogglerProps({onClick, ...props} = {}) {
    return {
      'aria-pressed': on,
      onClick: callAll(onClick, toggle),
      ...props,
    }
  }

  function getResetterProps({onClick, ...props} = {}) {
    return {
      onClick: callAll(onClick, reset),
      ...props,
    }
  }

  return {
    on,
    reset,
    toggle,
    getTogglerProps,
    getResetterProps,
  }
}

function Toggle({on: controlledOn, onChange}) {
  const {on, getTogglerProps} = useToggle({on: controlledOn, onChange})
  const props = getTogglerProps({on})
  return <Switch {...props} />
}

function App() {
  const [bothOn, setBothOn] = React.useState(false)
  const [timesClicked, setTimesClicked] = React.useState(0)

  function handleToggleChange(state, action) {
    if (action.type === actionTypes.toggle && timesClicked > 4) {
      return
    }
    setBothOn(state.on)
    setTimesClicked(c => c + 1)
  }

  function handleResetClick() {
    setBothOn(false)
    setTimesClicked(0)
  }

  return (
    <div>
      <div>
        <Toggle on={bothOn} onChange={handleToggleChange} />
        <Toggle on={bothOn} onChange={handleToggleChange} />
      </div>
      {timesClicked > 4 ? (
        <div data-testid="notice">
          Whoa, you clicked too much!
          <br />
        </div>
      ) : (
        <div data-testid="click-count">Click count: {timesClicked}</div>
      )}
      <button onClick={handleResetClick}>Reset</button>
      <hr />
      <div>
        <div>Uncontrolled Toggle:</div>
        <Toggle
          onChange={(...args) =>
            console.info('Uncontrolled Toggle onChange', ...args)
          }
        />
      </div>
    </div>
  )
}

export default App

Nếu component là uncontrolled (check bằng flag onIsControlled) thì hàm toggle bên trong useToggle chúng ta sử dụng dispatch như ban đầu. Ngược lại, với controlled component, chúng ta gọi hàm onChange (được truyền vào ) với hai tham số là stateaction.

function dispatchWithOnChange(action) {
    if (!onIsControlled) {
      dispatch(action)
    }
    onChange?.(reducer({...state, on}, action), action)
}

Hàm reducer({...state, on}, action) sẽ nhận vào state {...state, on} action. Sau đó sẽ gọi hàm này, nó trả về state mới. Và chúng ta sử dụng giá trị state mới này để gọi hàm setBothOn để control props on của <Toggle /> component.

function handleToggleChange(state, action) {
    if (action.type === actionTypes.toggle && timesClicked > 4) {
      return
    }
    setBothOn(state.on)
    setTimesClicked(c => c + 1)
}

Trên đây là cách để bạn kết hợp state reducercontrol props để tạo nên những component được tái sử dụng với sự linh hoạt cao hơn. Happy coding!

Nguồn: Epic React by Kent C.Dodds

Leave a Reply

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