更新
切换受控状态写了个hook 在这里
拿这个配合普通受控组件就行 根本不需要那么麻烦 之前是陷入误区了
需求
主题工具项目,要求可以编辑input内容,防抖地将变更同步到展示的组件中。此外切换主题时,input的值和展示的组件都要和主题保持一致。
简化需求如下
这要求输入框在防抖时间内非受控,在之外的时间受控,且不能丢失焦点。
简单实现
下面这段代码简单地完成了需求,实现了input组件在受控/非受控间的切换
export default function App() {
const initText = '初始内容'
const [text, setText] = useState(initText)
return (
<div className='flex flex-col gap-4 m-4 w-1/4 items-start'>
<Button onClick={() => setText(initText)} type='primary'>重置文字内容</Button>
<span><span className='mr-4'>文字内容:</span>{text}</span>
<span>
防抖input:
<DebounceInput value={text} onChange={setText}></DebounceInput>
</span>
</div>
)
}
function DebounceInput({ value, onChange }: { value: string, onChange: (val: string) => void }) {
const [text, setText] = useState(value)
const debounceText = useDebounceValue(text) // 取得一个状态的防抖版本
const isFirstMountedRef = useIsFirstMounted() // 判断是否第一次渲染
useEffect(() => {
if (!isFirstMountedRef.current) {
onChange(debounceText)
}
}, [debounceText, onChange, isFirstMountedRef])
useEffect(() => {
if (!isFirstMountedRef.current) {
setText(value)
}
}, [value, onChange, isFirstMountedRef])
return (
<span className='flex whitespace-nowrap gap-4'>
防抖input
<Input value={text} onChange={e => setText(e.target.value)}></Input>
</span>
)
}
/** 取得一个状态的防抖版本 */
function useDebounceValue(value: string) {
const [debounceValue, setDebounceValue] = useState(value)
useEffect(() => {
const timer = setTimeout(() => setDebounceValue(value), 200)
return () => {
clearTimeout(timer)
}
}, [value])
return debounceValue
}
/** 判断一个组件是否第一次渲染 */
function useIsFirstMounted() {
const isFirstMountedRef = useRef(true)
useEffect(() => {
if (isFirstMountedRef.current) {
return () => {
isFirstMountedRef.current = false
}
}
})
return isFirstMountedRef
}
优化
你可能不需要 Effect – React 中文文档
受文档启发,我发现DebounceInput响应value的变化更新状态是不合理的,做出以下优化
- 从DebounceInput内抽离非受控组件DebounceInputInner,仅从接受初始值和变化函数
- DebounceInput监听输入框的值,与value不相等时,以value为初始值重建DebounceInputInner,使它的表现如同受控组件
最终结果
export default function App() {
// 同上
}
function DebounceInput({ value, onChange }: { value: string, onChange: (val: string) => void }) {
const prevValueRef = useRef(value)
const onInnerChange = useCallback<(val: string) => void>(val => {
prevValueRef.current = val
onChange(val)
}, [onChange])
const keyRef = useRef<number>()
if (prevValueRef.current !== value) {
keyRef.current = Math.random()
}
return <DebounceInputInner key={keyRef.current} initValue={value} onChange={onInnerChange}></DebounceInputInner>
}
function DebounceInputInner({ initValue, onChange }: { initValue: string, onChange: (val: string) => void }) {
const [value, setValue] = useState(initValue)
const debounceValue = useDebounceValue(value)
const isFirstMountedRef = useRef(true)
const prevOnChangeRef = useRef(onChange)
useEffect(() => {
if (isFirstMountedRef.current) {
isFirstMountedRef.current = false
} else {
if(prevOnChangeRef.current !== onChange){
prevOnChangeRef.current = onChange
} else {
// 组件第一次渲染和onChange变化引发effect时都不调用onChange
onChange(debounceValue)
}
}
}, [debounceValue, onChange])
return <Input defaultValue={initValue} onChange={e => setValue(e.target.value)}></Input>
}
/** 取得一个值的防抖版本 */
function useDebounceValue(value: string) {
// 同上
}
不足
优化后的组件虽然消除了冗余的状态变更,却具有一些新的不足
- 组件在构建时使用了多个useRef,在理解和改造上有一定阻碍。
- 组件可以正常输入修改,但如果value因为外部原因变化(例如重置按钮修改value值,这不是来自DebounceInput提交的修改),则会导致输入框重建,丢失焦点。