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

Fluent UI 9 TextArea与useForm集成时失焦及输入异常问题求助

问题描述
  • 当TextArea组件触发验证错误时会失去焦点;
  • 手动调用ref.focus()恢复焦点后,输入会忽略之前的内容,新字符从第0位开始。

相关代码

TextAreaField组件代码

import * as React from "react";
import {
  makeStyles,
  shorthands,
  useId,
  Input,
  Label,
  Field,
  Textarea,
} from "@fluentui/react-components";
import {
  Controller,
  ControllerFieldState,
  ControllerRenderProps,
  FieldValues,
  UseFormStateReturn,
  useFormContext,
} from "react-hook-form";

interface inputField {
  name: string;
  placeholder?: string;
  label?: string;
  icon?: React.ReactNode;
  isRequired?: boolean;
}

const useStyles = makeStyles({
  root: {
    display: "flex",
    flexDirection: "column",
    ...shorthands.gap("20px"),
    maxWidth: "900px",
    width: "100%",
    "> div": {
      display: "flex",
      flexDirection: "column",
      ...shorthands.gap("3px"),
    },
  },
});

export const TextAreaField = (props: inputField) => {
  const styles = useStyles();

  const { control, formState, register } = useFormContext();
  const err = formState.errors[props.name]?.message as string | null;

  console.log(formState);

  const inputId = useId("TextArea");

  return (
    <Controller
      key={"TextArea"}
      control={control}
      name={props.name}
      defaultValue={formState.defaultValues}
      render={function ({
        field,
        fieldState,
        formState,
      }: {
        field: ControllerRenderProps<FieldValues, string>;
        fieldState: ControllerFieldState;
        formState: UseFormStateReturn<FieldValues>;
      }): React.ReactElement<any, string | React.JSXElementConstructor<any>> {
        return (
          <div key={"This is Text Area"} className={styles.root}>
            <div key={"InnerTable"}>
              <Label
                className={props.isRequired ? "red-star" : ""}
                style={{ fontWeight: "bold", letterSpacing: "0.3px" }}
                htmlFor={props.name}
              >
                {props.label}
              </Label>
              <Field
                key={"Error"}
                validationMessage={
                  formState.errors[props.name]?.message as string
                }
              >
                <Textarea
                  key={"NoRender"}
                  size="medium"
                  id={inputId}
                  placeholder={props.placeholder}
                  {...field}
                  ref={(inputRef) => {
                    if (inputRef && formState.name === props.name) {
                      inputRef.focus();
                      inputRef.autofocus = true;
                    }
                  }}
                />
              </Field>
            </div>
          </div>
        );
      }}
    ></Controller>
  );
};

export default React.memo(TextAreaField);

组件配置代码

const AddAndUpdateAddressWidgets: FormField[] = [
  {
    componentType: "TextField",
    order: 2,
    props: {
      errorMessage: "",
      name: "address_name",
      label: "address_name",
      options: [],
      placeholder: "",
      disabled: false,
      width: "",
    },
  },
  {
    componentType: "TextField",
    order: 3,
    props: {
      errorMessage: "",
      name: "pin_code",
      label: "Pincode",
      options: [],
      placeholder: "",
      disabled: false,
      width: "",
    },
  },
  {
    componentType: "TextField",
    order: 4,
    props: {
      errorMessage: "",
      name: "district_name",
      label: "District",
      options: [],
      placeholder: "",
      disabled: false,
      width: "",
    },
  },
  {
    componentType: "TextAreaField",
    order: 5,
    props: {
      errorMessage: "",
      name: "address_line_1",
      label: "Address Line 1",
      options: [],
      placeholder: "",
      disabled: false,
      width: "",
    },
  },
  {
    componentType: "TextAreaField",
    order: 6,
    props: {
      errorMessage: "",
      name: "address_line_2",
      label: "Address Line 2",
      options: [],
      placeholder: "",
      disabled: false,
      width: "",
    },
  },
  {
    componentType: "TextAreaField",
    order: 7,
    props: {
      errorMessage: "",
      name: "office_remarks",
      label: "office_remarks",
      options: [],
      placeholder: "",
      disabled: false,
      width: "",
    },
  },
];

export default AddAndUpdateAddressWidgets;

组件循环渲染代码

{sortedAddAndUpdateWidgets.map(
  (
    field: { componentType: string | number; props: any },
    index: any
  ) => {
    const Component = React.memo(
      componentRegistry[field.componentType]
    );
    return (
      <Component
        key={index}
        {...field.props}
      />
    );
  }
)}

修复方案

问题根源

  1. defaultValue设置错误:Controller传入了整个表单默认值对象,而非当前字段的对应值,导致TextArea初始化值异常。
  2. 焦点处理逻辑不当:直接在ref回调中调用focus会触发不必要重渲染,且未保留光标位置;同时错误使用了不存在的formState.name属性判断错误字段。
  3. 元素关联错误:Label的htmlFor与Textarea的id不匹配,破坏表单元素语义关联。
  4. Key值冲突:多个TextAreaField使用固定key,导致React渲染异常。

修复后的TextAreaField组件

import * as React from "react";
import {
  makeStyles,
  shorthands,
  useId,
  Label,
  Field,
  Textarea,
} from "@fluentui/react-components";
import {
  Controller,
  ControllerFieldState,
  ControllerRenderProps,
  FieldValues,
  useFormContext,
} from "react-hook-form";

interface inputField {
  name: string;
  placeholder?: string;
  label?: string;
  icon?: React.ReactNode;
  isRequired?: boolean;
}

const useStyles = makeStyles({
  root: {
    display: "flex",
    flexDirection: "column",
    ...shorthands.gap("20px"),
    maxWidth: "900px",
    width: "100%",
    "> div": {
      display: "flex",
      flexDirection: "column",
      ...shorthands.gap("3px"),
    },
  },
});

export const TextAreaField = (props: inputField) => {
  const styles = useStyles();
  const { control, formState } = useFormContext();
  const inputId = useId("TextArea");
  const textareaRef = React.useRef<HTMLTextAreaElement>(null);
  const isFieldInvalid = !!formState.errors[props.name];

  React.useEffect(() => {
    // 字段验证失败时恢复焦点,并将光标定位到内容末尾
    if (isFieldInvalid && textareaRef.current) {
      textareaRef.current.focus();
      const contentLength = textareaRef.current.value.length;
      textareaRef.current.setSelectionRange(contentLength, contentLength);
    }
  }, [isFieldInvalid, props.name]);

  return (
    <Controller
      key={props.name}
      control={control}
      name={props.name}
      defaultValue={formState.defaultValues?.[props.name] ?? ""}
      render={({ field, fieldState }) => {
        return (
          <div className={styles.root}>
            <div>
              <Label
                className={props.isRequired ? "red-star" : ""}
                style={{ fontWeight: "bold", letterSpacing: "0.3px" }}
                htmlFor={inputId}
              >
                {props.label}
              </Label>
              <Field validationMessage={fieldState.error?.message}>
                <Textarea
                  size="medium"
                  id={inputId}
                  placeholder={props.placeholder}
                  {...field}
                  ref={(el) => {
                    field.ref(el);
                    textareaRef.current = el;
                  }}
                />
              </Field>
            </div>
          </div>
        );
      }}
    />
  );
};

export default React.memo(TextAreaField);

额外优化:循环渲染的Key值

将循环渲染的key从index改为字段唯一标识field.props.name,避免组件复用导致的状态异常:

{sortedAddAndUpdateWidgets.map((field) => {
  const Component = componentRegistry[field.componentType];
  return (
    <Component
      key={field.props.name}
      {...field.props}
    />
  );
})}

内容的提问来源于stack exchange,提问作者Aadarsh Kumar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 03:25:11