Tự học ReactJS: React Patterns – Props collection và getter

Ở bài trước chúng ta đã bàn luận về Toggle component và cách để làm nó dễ dàng sử dụng hơn bằng functional component. Trong này bài, chúng ta tiếp tục sử dụng Toggle làm ví dụ nhưng với cách khác là dùng props collections và getter để cài đặt nó với custom hook (useToggle).

Một số thư viện như downshiftreact-table sử dụng pattern này để tạo nên reusable components. Ví dụ như sau:

import * as React from 'react'
import {render} from 'react-dom'
import {useCombobox} from 'downshift'
// items = ['Neptunium', 'Plutonium', ...]
import {items, menuStyles, comboboxStyles} from './utils'

function DropdownCombobox() {
  const [inputItems, setInputItems] = useState(items)
  const {
    isOpen,
    selectedItem,
    getToggleButtonProps,
    getLabelProps,
    getMenuProps,
    getInputProps,
    getComboboxProps,
    highlightedIndex,
    getItemProps,
  } = useCombobox({
    items: inputItems,
    onInputValueChange: ({inputValue}) => {
      setInputItems(
        items.filter(item =>
          item.toLowerCase().startsWith(inputValue.toLowerCase()),
        ),
      )
    },
  })

  return (
    <>
      <label {...getLabelProps()}>Choose an element:</label>
      <div style={comboboxStyles} {...getComboboxProps()}>
        <input {...getInputProps()} />
        <button
          type="button"
          {...getToggleButtonProps()}
          aria-label={'toggle menu'}
        >
          &#8595;
        </button>
      </div>
      <ul {...getMenuProps()} style={menuStyles}>
        {isOpen &&
          inputItems.map((item, index) => (
            <li
              style={
                highlightedIndex === index ? {backgroundColor: '#bde4ff'} : {}
              }
              key={`${item}${index}`}
              {...getItemProps({item, index})}
            >
              {item}
            </li>
          ))}
      </ul>
    </>
  )
}

render(<DropdownCombobox />, document.getElementById('root'))

Các hàm getToggleButtonProps, getLabelProps, getMenuProps, getInputProps, getComboboxProps được gọi là props getter, chúng lấy state, props từ useCombobox, sau đó chúng ta lấy các giá trị này truyền vào component của chúng ta.

Hay với react-table:

const {
    getTableProps,
    getTableBodyProps,
    headerGroups,
    rows,
    prepareRow,
  } = useTable({
    columns,
    data,
  })

Với props getter bạn cũng có thể dễ dàng thêm các aria- attribute vào component của bạn. ARIA là những attribute truyền vào DOM để hỗ trợ cho người khiếm thị. Họ không thấy các button, element trên trang web. Họ đơn giản sử dụng các thiết bị đọc màn hình và sử dụng phím tab để di chuyển qua lại giữa các element. Khi họ tab tới đâu thì âm thanh sẽ phát ra để họ biết họ đang ở button nào.

Sử dụng props collection cho Toggle

Props collection là tập hợp tất cả các props của một component lại và return chúng về để một component khác có thể lấy ra xài được.

Ví dụ như sau:

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

function useToggle() {
  const [on, setOn] = React.useState(false)
  const toggle = () => setOn(!on)

  return {
    on,
    toggle,
    togglerProps: {
      'aria-pressed': on, // đã có sẵn aria-pressed cho người khiếm thị
      onClick: toggle,
    },
  }
}

function App() {
  const {on, togglerProps} = useToggle()
  return (
    <div>
      <Switch on={on} {...togglerProps} />
      <hr />
      <button aria-label="custom-button" {...togglerProps}>
        {on ? 'on' : 'off'}
      </button>
    </div>
  )
}

togglerProps là pattern props collections, nó là một object tập hợp tất cả các props của useToggle. Chúng ta có thể truy xuất và truyền nó vào component bên ngoài bằng cách:

const {on, togglerProps} = useToggle()

// Truyền vào Switch và button
<Switch on={on} {...togglerProps} />
<button aria-label="custom-button" {...togglerProps}>

Sử dụng props getter cho Toggle

Giả sử bạn muốn truyền hàm onClick vào toggle button như sau:

function App() {
  const {on, togglerProps} = useToggle()
  return (
    <div>
      <Switch on={on} {...togglerProps} />
      <hr />
      <button
        aria-label="custom-button"
        {...togglerProps}
        onClick={() => console.log('Toggle button is clicked')}
      >
        {on ? 'on' : 'off'}
      </button>
    </div>
  )
}

Nhưng với cách sử dụng này khi bạn click vào button nó không toggle được cái Switch nữa. Bởi vì hàm onClick chúng ta thêm vào đã ghi đè lên hàm onClick bên trong togglerProps. Để fix lỗi này, có thể bạn sẽ để hàm onClick trước togglerProps như sau:

<button
   aria-label="custom-button"
   onClick={() => console.log('Toggle button is clicked')}
   {...togglerProps}
 >
   {on ? 'on' : 'off'}
</button>

Với ý tưởng này cũng ko được vì hàm onClick lại bị ghi đè. Cho nên có một cách, sử dụng props getter linh hoạt hơn như sau:

function App() {
  const {on, getTogglerProps} = useToggle()
  return (
    <div>
      <Switch {...getTogglerProps({on})} />
      <hr />
      <button
        {...getTogglerProps({
          'aria-label': 'custom-button',
          onClick: () => console.info('onButtonClick'),
          id: 'custom-button-id',
        })}
      >
        {on ? 'on' : 'off'}
      </button>
    </div>
  )
}

Chúng ta tạo thêm một hàm getTogglerProps trong hook useToggle và chúng ta sẽ truyền hàm onClick vào như một tham số. Toàn bộ code thay đổi như sau:

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

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

function useToggle() {
  const [on, setOn] = React.useState(false)
  const toggle = () => setOn(!on)

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

  return {
    on,
    toggle,
    getTogglerProps,
  }
}

function App() {
  const {on, getTogglerProps} = useToggle()
  return (
    <div>
      <Switch {...getTogglerProps({on})} />
      <hr />
      <button
        {...getTogglerProps({
          'aria-label': 'custom-button',
          onClick: () => console.info('onButtonClick'),
          id: 'custom-button-id',
        })}
      >
        {on ? 'on' : 'off'}
      </button>
    </div>
  )
}

export default App

Đây được gọi là pattern props getter (getTogglerProps) và chúng ta có thể tự do truyền những props như: aria-label, onClick, id, … Và tất cả xử lí chúng ta đặt bên trong hook useToggle. Ở đây chúng ta có một helper callAll, đơn giản nó chỉ call tất cả functions truyền vào. Khi bạn click vào button toggle, phía dưới chúng ta sẽ gọi hàm onClick (mình truyền vào) và hàm toggle của Toggle component:

onClick: callAll(onClick, toggle)

Kết quả sau cùng khi áp dụng pattern props getter cho component Toggle bạn tham khảo tại đây.

Nguồn: Epic React by Kent C.Dodds

Leave a Reply

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