Ở 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ư downshift và react-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'}
>
↓
</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