1.组件开发思路
设计组件的基本思路,我们通常考虑一个组件是否应该为受控组件或非受控组件,在我们日常使用antd组件时,会巧妙的发现,antd的所有组件都是支持受控和非受控的,使用者的场景可以更加丰富,因为我们今天就来探索如何设计一款支持受控和非受控的组件。
常规思路:
import React, { useState } from 'react';
import { Button, Form, Input, Select } from 'antd';
const { Option } = Select;
type Currency = 'rmb' | 'dollar';
interface PriceValue {
number?: number;
currency?: Currency;
}
interface PriceInputProps {
id?: string;
value?: PriceValue;
onChange?: (value: PriceValue) => void;
}
const PriceInput: React.FC<PriceInputProps> = (props) => {
const { id, value = {}, onChange } = props;
const [number, setNumber] = useState(0);
const [currency, setCurrency] = useState<Currency>('rmb');
const triggerChange = (changedValue: { number?: number; currency?: Currency }) => {
onChange?.({ number, currency, ...value, ...changedValue });
};
const onNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newNumber = parseInt(e.target.value || '0', 10);
if (Number.isNaN(number)) {
return;
}
if (!('number' in value)) {
setNumber(newNumber);
}
triggerChange({ number: newNumber });
};
const onCurrencyChange = (newCurrency: Currency) => {
if (!('currency' in value)) {
setCurrency(newCurrency);
}
triggerChange({ currency: newCurrency });
};
return (
<span id={id}>
<Input
type="text"
value={value.number || number}
onChange={onNumberChange}
style={{ width: 100 }}
/>
<Select
value={value.currency || currency}
style={{ width: 80, margin: '0 8px' }}
onChange={onCurrencyChange}
>
<Option value="rmb">RMB</Option>
<Option value="dollar">Dollar</Option>
</Select>
</span>
);
};
const App: React.FC = () => {
const onFinish = (values: any) => {
console.log('Received values from form: ', values);
};
const checkPrice = (_: any, value: { number: number }) => {
if (value.number > 0) {
return Promise.resolve();
}
return Promise.reject(new Error('Price must be greater than zero!'));
};
return (
<Form
name="customized_form_controls"
layout="inline"
onFinish={onFinish}
initialValues={{
price: {
number: 0,
currency: 'rmb',
},
}}
>
<Form.Item name="price" label="Price" rules={[{ validator: checkPrice }]}>
<PriceInput />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
};
export default App;
我们可以看到这里需要内部支持state和setState,核心是使用triggerChange方式来适配。
如果我们使用useControllableValue 会使代码量大幅的减少,同时达到同样的效果。
import React from 'react';
import { Button, Form, Input, Select } from 'antd';
import useControllableValue from 'ahooks';
const { Option } = Select;
type Currency = 'rmb' | 'dollar';
interface PriceValue {
number?: number;
currency?: Currency;
}
interface PriceInputProps {
id?: string;
value?: PriceValue;
onChange?: (value: PriceValue) => void;
}
const PriceInput: React.FC<PriceInputProps> = (props: PriceInputProps) => {
const { id } = props;
// 使用 useControllableValue 管理组件状态
const [value, setValue] = useControllableValue<PriceValue>(props, {
defaultValue: { number: 0, currency: 'rmb' },
});
const onNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newNumber = parseInt(e.target.value || '0', 10);
if (Number.isNaN(newNumber)) {
return;
}
setValue({ ...value, number: newNumber });
};
const onCurrencyChange = (newCurrency: Currency) => {
setValue({ ...value, currency: newCurrency });
};
return (
<span id={id}>
<Input
type="text"
value={value?.number || 0}
onChange={onNumberChange}
style={{ width: 100 }}
/>
<Select
value={value?.currency || 'rmb'}
style={{ width: 80, margin: '0 8px' }}
onChange={onCurrencyChange}
>
<Option value="rmb">RMB</Option>
<Option value="dollar">Dollar</Option>
</Select>
</span>
);
};
const App: React.FC = () => {
const onFinish = (values: any) => {
console.log('Received values from form: ', values);
};
const checkPrice = (_: any, value: { number: number }) => {
if (value.number > 0) {
return Promise.resolve();
}
return Promise.reject(new Error('Price must be greater than zero!'));
};
return (
<Form
name="customized_form_controls"
layout="inline"
onFinish={onFinish}
initialValues={{
price: {
number: 0,
currency: 'rmb',
},
}}
>
<Form.Item name="price" label="Price" rules={[{ validator: checkPrice }]}>
<PriceInput />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
};
export default App;
两个案例的对比,能明显感受到useControllableValue的好处,我们不需要自己再去设置内部的state,我们只需要把外部传入的value和onChange的props透传到useControllableValue即可。后续我们在setValue时候,只需要像更新内部setSate一样。就可以达到受控和非受控的效果。 我认为设计的很巧妙,因此我去看了ahooks的具体实现原理如下:
首先传统的开发思路模式,在做受控和非受控时思路局限于创建一个本地的state,如果父组件有value就用父组件的value,onChange的时候触发修改,如果没有传入就使用本地的state更新 核心代码如下
if (!('number' in value)) { setNumber(newNumber); } triggerChange({ number: newNumber });
然后在useControllableValue的实现过程中放弃了这一思路,采用useRef来实现, 核心代码如下
function useControllableValue<T = any>(defaultProps: Props, options: Options<T> = {}) {
const props = defaultProps ?? {};
const {
defaultValue,
defaultValuePropName = 'defaultValue',
valuePropName = 'value',
trigger = 'onChange',
} = options;
const value = props[valuePropName] as T;
const isControlled = Object.prototype.hasOwnProperty.call(props, valuePropName);
const initialValue = useMemo(() => {
if (isControlled) {
return value;
}
if (Object.prototype.hasOwnProperty.call(props, defaultValuePropName)) {
return props[defaultValuePropName];
}
return defaultValue;
}, []);
const stateRef = useRef(initialValue);
if (isControlled) {
stateRef.current = value;
}
const update = useUpdate();
function setState(v: SetStateAction<T>, ...args: any[]) {
const r = isFunction(v) ? v(stateRef.current) : v;
if (!isControlled) {
stateRef.current = r;
update();
}
if (props[trigger]) {
props[trigger](r, ...args);
}
}
return [stateRef.current, useMemoizedFn(setState)] as const;
}
这里我们可以看到本质思路外部是采用stateRef.current 作为页面的渲染,从这个hooks的设计思路中可以巧妙的看到 如果!isControlled 是受控组件,只需要内部更新value,手动触发update,update的思路也十分巧妙
import { useCallback, useState } from 'react';
const useUpdate = () => {
const [, setState] = useState({});
return useCallback(() => setState({}), []);
};
export default useUpdate;
只需要刷新触发当前组件的reRender stateRef.current的值更新后,外部使用该hooks的值也会同步更新。
如果是受控组件,只需要每次更新触发onChange即可
if (props[trigger]) { props[trigger](r, ...args); }
每次onChange后 会更新props,value触发最新值,每次render时候都赋值为最新即可
if (isControlled) { stateRef.current = value; }
总结: 在同学们去开发组件,想要设计为value和onChange时,推荐将组件设计为受控和非受控的支持,更加的便于维护,useControllableValue是得力的助手帮助我们实现。