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
- Downshift của Kent
- @reach/listbox
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 on
và onChange
. Hai props này hoạt động giống như value
và onChange
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à state và action.
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}
và 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 reducer và control 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