如何重构两个逻辑近乎相同的React表单字段组件?
重构DateTimeFormField与TimeFormField组件
我有两个组件DateTimeFormField和TimeFormField,逻辑与输出几乎完全一致,希望对它们进行重构。曾考虑导出DateTimeFormField内部的函数,但这并非优雅方案,因为两个组件仍存在重复的变量声明逻辑。调研发现自定义钩子或许能解决代码重复问题,但由于组件是无状态的,不确定此场景下使用自定义钩子是否属于过度设计。
现有组件代码
DateTimeFormField.tsx
function DateTimeFormField({ fieldName, flatFieldName, validationMessage, value, onChange, onBlur }: FieldProps) { const valueAsStr = value === undefined ? "" : value as string; const dateFromValue = valueAsStr.substring(0, 10); const timeFromValue = valueAsStr.substring(11, 16); const todayDateObj = new Date(); const todayDate = todayDateObj.toISOString().slice(0, 10); function onDateChange(dateUpdate: string) { const date = dateUpdate || "0000-00-00"; const time = timeFromValue || "00:00"; return onChange(flatFieldName, date + "T" + time + ":00+00:00"); } function onTimeChange(timeUpdate: string) { const date = dateFromValue || todayDate; const time = timeUpdate || "00:00"; return onChange(flatFieldName, date + "T" + time + ":00+00:00"); } function onTimeBlur(timeUpdate: string, fieldName: string) { onTimeChange(timeUpdate); onBlur(fieldName); } return ( <FormGroup validationMessage={validationMessage} data-element="date-time-form-field-form-group"> <FieldLabel fieldName={fieldName} /> <div className="date-time-combo"> <DatePicker inputProps={{"data-element": "date-form-field-input"}} dateFormat="YYYY-MM-DD" initialValue={dateFromValue} onChange={(date) => onDateChange(date.isoFormattedDate)} onBlur={() => onBlur(flatFieldName)} /> <TimePicker data-element="time-form-field-input" timeFormat="HH:mm" initialValue={timeFromValue} onSelect={(time) => onTimeChange(time.rawValue)} onBlur={(time) => onTimeBlur(time.rawValue, flatFieldName)} /> </div> </FormGroup> ); } export default memo(DateTimeFormField);
TimeFormField.tsx
function TimeFormField({ fieldName, flatFieldName, validationMessage, value, onChange, onBlur }: FieldProps) { const valueAsStr = value === undefined ? "" : value as string; const dateFromValue = valueAsStr.substring(0, 10); const timeFromValue = valueAsStr.substring(11, 16); const todayDateObj = new Date(); const todayDate = todayDateObj.toISOString().slice(0, 10); function onTimeChange(timeUpdate: string) { const date = dateFromValue || todayDate; const time = timeUpdate || "00:00"; return onChange(flatFieldName, date + "T" + time + ":00+00:00"); } function onTimeBlur(timeUpdate: string, fieldName: string) { onTimeChange(timeUpdate); onBlur(fieldName); } return ( <FormGroup validationMessage={validationMessage} data-element="time-form-field-form-group"> <FieldLabel fieldName={fieldName} /> <div className="time-combo"> <TimePicker data-element="time-form-field-input" timeFormat="HH:mm" initialValue={timeFromValue} onSelect={(time) => onTimeChange(time.rawValue)} onBlur={(time) => onTimeBlur(time.rawValue, flatFieldName)} /> </div> </FormGroup> ); } export default memo(TimeFormField);
重构方案
方案一:自定义Hook提取共享逻辑
这种方式完全不算过度设计。自定义Hook的核心作用就是提取组件间可复用的逻辑,不管组件是否有状态。通过Hook把变量计算、回调函数等共享逻辑抽离,让组件只专注于渲染,代码结构更清晰。
第一步:创建自定义Hook
// useDateTimeFieldLogic.ts import { useMemo } from 'react'; interface UseDateTimeFieldLogicProps { value: any; flatFieldName: string; onChange: (field: string, value: string) => void; onBlur: (field: string) => void; } export function useDateTimeFieldLogic({ value, flatFieldName, onChange, onBlur }: UseDateTimeFieldLogicProps) { // 共享的变量计算 const valueAsStr = useMemo(() => value === undefined ? "" : String(value), [value]); const dateFromValue = useMemo(() => valueAsStr.substring(0, 10), [valueAsStr]); const timeFromValue = useMemo(() => valueAsStr.substring(11, 16), [valueAsStr]); const todayDate = useMemo(() => new Date().toISOString().slice(0, 10), []); // 共享的回调函数 const onTimeChange = (timeUpdate: string) => { const date = dateFromValue || todayDate; const time = timeUpdate || "00:00"; onChange(flatFieldName, `${date}T${time}:00+00:00`); }; const onTimeBlur = (timeUpdate: string) => { onTimeChange(timeUpdate); onBlur(flatFieldName); }; // DateTimeFormField独有的回调 const onDateChange = (dateUpdate: string) => { const date = dateUpdate || "0000-00-00"; const time = timeFromValue || "00:00"; onChange(flatFieldName, `${date}T${time}:00+00:00`); }; return { dateFromValue, timeFromValue, todayDate, onDateChange, onTimeChange, onTimeBlur }; }
第二步:修改原有组件
DateTimeFormField.tsx
import { memo } from 'react'; import { useDateTimeFieldLogic } from './useDateTimeFieldLogic'; function DateTimeFormField({ fieldName, flatFieldName, validationMessage, value, onChange, onBlur }: FieldProps) { const { dateFromValue, timeFromValue, onDateChange, onTimeChange, onTimeBlur } = useDateTimeFieldLogic({ value, flatFieldName, onChange, onBlur }); return ( <FormGroup validationMessage={validationMessage} data-element="date-time-form-field-form-group"> <FieldLabel fieldName={fieldName} /> <div className="date-time-combo"> <DatePicker inputProps={{"data-element": "date-form-field-input"}} dateFormat="YYYY-MM-DD" initialValue={dateFromValue} onChange={(date) => onDateChange(date.isoFormattedDate)} onBlur={() => onBlur(flatFieldName)} /> <TimePicker data-element="time-form-field-input" timeFormat="HH:mm" initialValue={timeFromValue} onSelect={(time) => onTimeChange(time.rawValue)} onBlur={(time) => onTimeBlur(time.rawValue)} /> </div> </FormGroup> ); } export default memo(DateTimeFormField);
TimeFormField.tsx
import { memo } from 'react'; import { useDateTimeFieldLogic } from './useDateTimeFieldLogic'; function TimeFormField({ fieldName, flatFieldName, validationMessage, value, onChange, onBlur }: FieldProps) { const { timeFromValue, onTimeChange, onTimeBlur } = useDateTimeFieldLogic({ value, flatFieldName, onChange, onBlur }); return ( <FormGroup validationMessage={validationMessage} data-element="time-form-field-form-group"> <FieldLabel fieldName={fieldName} /> <div className="time-combo"> <TimePicker data-element="time-form-field-input" timeFormat="HH:mm" initialValue={timeFromValue} onSelect={(time) => onTimeChange(time.rawValue)} onBlur={(time) => onTimeBlur(time.rawValue)} /> </div> </FormGroup> ); } export default memo(TimeFormField);
方案二:通用组件+配置参数
如果觉得自定义Hook有点繁琐,也可以将两个组件合并为一个通用组件,通过配置参数控制是否显示日期选择器,这样也能大幅减少重复代码。
// BaseDateTimeField.tsx import { memo } from 'react'; interface BaseDateTimeFieldProps extends FieldProps { showDatePicker: boolean; containerClassName: string; dataElementSuffix: string; } function BaseDateTimeField({ fieldName, flatFieldName, validationMessage, value, onChange, onBlur, showDatePicker, containerClassName, dataElementSuffix }: BaseDateTimeFieldProps) { const valueAsStr = value === undefined ? "" : String(value); const dateFromValue = valueAsStr.substring(0, 10); const timeFromValue = valueAsStr.substring(11, 16); const todayDate = new Date().toISOString().slice(0, 10); const onTimeChange = (timeUpdate: string) => { const date = dateFromValue || todayDate; const time = timeUpdate || "00:00"; onChange(flatFieldName, `${date}T${time}:00+00:00`); }; const onTimeBlur = (timeUpdate: string) => { onTimeChange(timeUpdate); onBlur(flatFieldName); }; const onDateChange = (dateUpdate: string) => { const date = dateUpdate || "0000-00-00"; const time = timeFromValue || "00:00"; onChange(flatFieldName, `${date}T${time}:00+00:00`); }; return ( <FormGroup validationMessage={validationMessage} data-element={`${dataElementSuffix}-form-field-form-group`}> <FieldLabel fieldName={fieldName} /> <div className={containerClassName}> {showDatePicker && ( <DatePicker inputProps={{"data-element": "date-form-field-input"}} dateFormat="YYYY-MM-DD" initialValue={dateFromValue} onChange={(date) => onDateChange(date.isoFormattedDate)} onBlur={() => onBlur(flatFieldName)} /> )} <TimePicker data-element="time-form-field-input" timeFormat="HH:mm" initialValue={timeFromValue} onSelect={(time) => onTimeChange(time.rawValue)} onBlur={(time) => onTimeBlur(time.rawValue)} /> </div> </FormGroup> ); } export default memo(BaseDateTimeField);
然后导出两个封装后的组件:
// DateTimeFormField.tsx import BaseDateTimeField from './BaseDateTimeField'; const DateTimeFormField = memo((props: FieldProps) => ( <BaseDateTimeField {...props} showDatePicker={true} containerClassName="date-time-combo" dataElementSuffix="date-time" /> )); export default DateTimeFormField;
// TimeFormField.tsx import BaseDateTimeField from './BaseDateTimeField'; const TimeFormField = memo((props: FieldProps) => ( <BaseDateTimeField {...props} showDatePicker={false} containerClassName="time-combo" dataElementSuffix="time" /> )); export default TimeFormField;
方案选择
- 如果后续可能新增更多类似的日期/时间组件(比如带时区的、仅日期的),自定义Hook的扩展性更强;
- 如果只是当前两个组件的复用,通用组件的方式更直接,代码量更少。
内容的提问来源于stack exchange,提问作者Mana
相关产品推荐
相关产品推荐

