受控与非受控概念
大白话来讲就是受父组件
控制和不受父组件
控制。
所以打造一个受控和非受控的组件就是考虑以下几点:
- 非受控:组件内部自行根据交互完成数据更新和视图更新
- 受控:组件内部数据由外部传入,展示外部数据,数据更新通知给外部
1、非受控版本案例
tsx
import React, { useEffect, useRef, useState } from "react@18"
import { createRoot } from 'react-dom@18'
function Calendar() {
const [value, setValue] = useState();
function changeValue(date: Date) {
setValue(date);
}
return <div>
{mergedValue?.toLocaleDateString()}
<div onClick={()=> {changeValue(new Date('2024-5-1'))}}>2023-5-1</div>
<div onClick={()=> {changeValue(new Date('2024-5-2'))}}>2023-5-2</div>
<div onClick={()=> {changeValue(new Date('2024-5-3'))}}>2023-5-3</div>
</div>
}
function App() {
return <>
<Calendar/>
</>
}
const app = document.getElementById('app');
const root = createRoot(app!)
root.render(<App />);
2、受控版本案例
tsx
import React, { useEffect, useRef, useState } from "react@18"
import { createRoot } from 'react-dom@18'
interface CalendarProps{
value?: Date; // 外部数据
defaultValue?: Date; // 默认值
onChange?: (date: Date) => void; // 数据改变事件
}
function Calendar(props: CalendarProps) {
const {
value: propsValue,
defaultValue,
onChange
} = props;
// 有外部值时,数据由外部初始化
const [value, setValue] = useState(() => {
if (propsValue !== undefined) {
return propsValue;
} else {
return defaultValue;
}
});
const isFirstRender = useRef(true);
// 当外部值由 xx --》 undefined的时候,是由受控 --〉 非受控,因此将数据控制权交会内部
useEffect(() => {
if(propsValue === undefined && !isFirstRender.current) {
setValue(propsValue);
}
isFirstRender.current = false;
}, [propsValue]);
// 外部数据存在,展示外部值,不存在,展示内部值
const mergedValue = propsValue === undefined ? value : propsValue;
// 外部数据存在,同步更新给外部,不存在,更新内部值
function changeValue(date: Date) {
if (propsValue === undefined) {
setValue(date);
}
onChange?.(date);
}
return <div>
{mergedValue?.toLocaleDateString()}
<div onClick={()=> {changeValue(new Date('2024-5-1'))}}>2023-5-1</div>
<div onClick={()=> {changeValue(new Date('2024-5-2'))}}>2023-5-2</div>
<div onClick={()=> {changeValue(new Date('2024-5-3'))}}>2023-5-3</div>
</div>
}
function App() {
return <>
<span>受控:</span>
<Calendar defaultValue={new Date('2024-5-1')} onChange={(date) => {
console.log(date.toLocaleDateString());
}}/>
<span>非受控:</span>
<Calendar/>
</>
}
const app = document.getElementById('app');
const root = createRoot(app!)
root.render(<App />);
3、最终案例
tsx
import React, { useEffect, useRef, useState } from "react@18"
import { createRoot } from 'react-dom@18'
interface CalendarProps{
value?: Date;
defaultValue?: Date;
onChange?: (date: Date) => void;
}
function Calendar(props: CalendarProps) {
const {
value: propsValue,
defaultValue,
onChange
} = props;
const [value, setValue] = useState(() => {
if (propsValue !== undefined) {
return propsValue;
} else {
return defaultValue;
}
});
const isFirstRender = useRef(true);
useEffect(() => {
if(propsValue === undefined && !isFirstRender.current) {
setValue(propsValue);
}
isFirstRender.current = false;
}, [propsValue]);
const mergedValue = propsValue === undefined ? value : propsValue;
function changeValue(date: Date) {
if (propsValue === undefined) {
setValue(date);
}
onChange?.(date);
}
return <div>
{mergedValue?.toLocaleDateString()}
<div onClick={()=> {changeValue(new Date('2024-5-1'))}}>2023-5-1</div>
<div onClick={()=> {changeValue(new Date('2024-5-2'))}}>2023-5-2</div>
<div onClick={()=> {changeValue(new Date('2024-5-3'))}}>2023-5-3</div>
</div>
}
function App() {
return <>
<span>受控:</span>
<Calendar defaultValue={new Date('2024-5-1')} onChange={(date) => {
console.log(date.toLocaleDateString());
}}/>
<span>非受控:</span>
<Calendar/>
</>
}
const app = document.getElementById('app');
const root = createRoot(app!)
root.render(<App />);
Hook封装
Hook将重复部分提取,change方法也是重复,它也是一个方法,所以通过装饰来扩展它的能力,变为setState。
ts
import { SetStateAction, useCallback, useEffect, useRef, useState } from "react"
export function useMergeState<T>(
defaultStateValue: T, // 内部兜底默认值
props?: {
defaultValue?: T, // 外部默认值
value?: T // 外部value
onChange?: (value: T) => void;
}
): [T, React.Dispatch<React.SetStateAction<T>>] {
const { defaultValue, value: propsValue, onChange } = props || {}
const isFirstRender = useRef(true)
const [stateValue, setStateValue] = useState<T>(() => {
if (propsValue !== undefined) {
return propsValue
} else if (defaultValue !== undefined) {
return defaultValue
} else {
return defaultStateValue
}
})
useEffect(() => {
if (propsValue === undefined && !isFirstRender.current) {
setStateValue(propsValue!)
}
isFirstRender.current = false
}, [propsValue])
const mergedValue = propsValue === undefined ? stateValue : propsValue
/**
* 装饰setState方法来扩展它的能力
*/
function isFunction(value: unknown): value is Function {
return typeof value === 'function'
}
const setState = useCallback((value: SetStateAction<T>) => {
let res = isFunction(value) ? value(stateValue) : value
if (propsValue === undefined) {
setStateValue(res)
}
onChange?.(res)
}, [stateValue])
return [mergedValue, setState]
}
参考文章: # 深入理解受控组件、非受控组件