You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何重构两个逻辑近乎相同的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 20:35:09