返回输入或textArea的元素的React / Typescript forwardRef类型

如何解决返回输入或textArea的元素的React / Typescript forwardRef类型

我正在尝试使用react和Typescript为我们的应用程序创建一个通用的文本输入组件。我希望它能够成为输入元素或基于给定道具的textarea元素。所以看起来像这样:

import {TextArea,Input} from 'ourComponentLibrary'

export const Component = forwardRef((props,ref) => {
  const Element = props.type === 'textArea' ? TextArea : Input

  return (
    <Element ref={ref} />
  )
})

此代码可以正常工作。但是,当尝试合并类型时,它变得有些麻烦。 ref类型应基于传递的HTMLInputElement属性为HTMLTextAreaElementtype。在我的脑海中,看起来像这样:

interface Props {
  ...
}

export const Component = forwardRef<
  HTMLInputElement | HTMLTextAreaElement,Props
>((props,ref) => {
  ...
});

但是我知道这并不是我真正需要的。因此,错误: Type 'HTMLInputElement' is missing the following properties from type 'HTMLTextAreaElement': cols,rows,textLength,wrap

总而言之,我希望类型对齐,以便如果type属性是textArea,则ref类型应该是HTMLTextAreaElement,并且如果类型prop是input,则引用类型应为HTMLInputAreaElement

有什么建议吗?

谢谢。

解决方法

虽然这绝对不能解决React.forwardProps的问题,但是另一种方法是解决该问题,而改用innerRef属性。然后,您可以在innerRef属性上强制执行类型。实现所需的结果,但输入灵活,开销少且无需实例化。

工作演示:

Edit Typescript - Switch Component


components / Label / index.tsx

import * as React from "react";
import { FC,LabelProps } from "~types";

/*
  Field label for form elements

  @param {string} name - form field name
  @param {string} label - form field label 
  @returns {JSX.Element}
*/
const Label: FC<LabelProps> = ({ name,label }) => (
  <label className="label" htmlFor={name}>
    {label}&#58;
  </label>
);

export default Label;

components / Fields / index.tsx

import * as React from "react";
import Label from "../Label";
import { FC,InputProps,TextAreaProps } from "~types";

/*
  Field elements for a form that are conditionally rendered by a fieldType
  of "input" or "textarea".

  @param {Object} props - properties for an input or textarea
  @returns {JSX.Element | null} 
*/
const Field: FC<InputProps | TextAreaProps> = (props) => {
  switch (props.fieldType) {
    case "input":
      return (
        <>
          <Label name={props.name} label={props.label} />
          <input
            ref={props.innerRef}
            name={props.name}
            className={props.className}
            placeholder={props.placeholder}
            type={props.type}
            value={props.value}
            onChange={props.onChange}
          />
        </>
      );
    case "textarea":
      return (
        <>
          <Label name={props.name} label={props.label} />
          <textarea
            ref={props.innerRef}
            name={props.name}
            className={props.className}
            placeholder={props.placeholder}
            rows={props.rows}
            cols={props.cols}
            value={props.value}
            onChange={props.onChange}
          />
        </>
      );
    default:
      return null;
  }
};

export default Field;

components / Form / index.tsx

import * as React from "react";
import Field from "../Fields";
import { FormEvent,FC,EventTargetNameValue } from "~types";

const initialState = {
  email: "",name: "",background: ""
};

const Form: FC = () => {
  const [state,setState] = React.useState(initialState);
  const emailRef = React.useRef<HTMLInputElement>(null);
  const nameRef = React.useRef<HTMLInputElement>(null);
  const bgRef = React.useRef<HTMLTextAreaElement>(null);

  const handleChange = React.useCallback(
    ({ target: { name,value } }: EventTargetNameValue) => {
      setState((s) => ({ ...s,[name]: value }));
    },[]
  );

  const handleReset = React.useCallback(() => {
    setState(initialState);
  },[]);

  const handleSubmit = React.useCallback(
    (e: FormEvent<HTMLFormElement>) => {
      e.preventDefault();

      const alertMessage = Object.values(state).some((v) => !v)
        ? "Must fill out all form fields before submitting!"
        : JSON.stringify(state,null,4);

      alert(alertMessage);
    },[state]
  );

  return (
    <form className="uk-form" onSubmit={handleSubmit}>
      <Field
        innerRef={emailRef}
        label="Email"
        className="uk-input"
        fieldType="input"
        type="email"
        name="email"
        onChange={handleChange}
        placeholder="Enter email..."
        value={state.email}
      />
      <Field
        innerRef={nameRef}
        label="Name"
        className="uk-input"
        fieldType="input"
        type="text"
        name="name"
        onChange={handleChange}
        placeholder="Enter name..."
        value={state.name}
      />
      <Field
        innerRef={bgRef}
        label="Background"
        className="uk-textarea"
        fieldType="textarea"
        rows={5}
        name="background"
        onChange={handleChange}
        placeholder="Enter background..."
        value={state.background}
      />
      <button
        className="uk-button uk-button-danger"
        type="button"
        onClick={handleReset}
      >
        Reset
      </button>
      <button
        style={{ float: "right" }}
        className="uk-button uk-button-primary"
        type="submit"
      >
        Submit
      </button>
    </form>
  );
};

export default Form;

types / index.ts

import type {
  FC,ChangeEvent,RefObject as Ref,FormEvent,ReactText
} from "react";

// custom utility types that can be reused
type ClassName = { className?: string };
type InnerRef<T> = { innerRef?: Ref<T> };
type OnChange<T> = { onChange: (event: ChangeEvent<T>) => void };
type Placeholder = { placeholder?: string };
type Value<T> = { value: T };

// defines a destructured event in a callback
export type EventTargetNameValue = {
  target: {
    name: string;
    value: string;
  };
};

/*
  Utility interface that constructs typings based upon passed in arguments

  @param {HTMLElement} E - type of HTML Element that is being rendered
  @param {string} F - the fieldType to be rendered ("input" or "textarea")
  @param {string} V - the type of value the field expects to be (string,number,etc)
*/
interface FieldProps<E,F,V>
  extends LabelProps,ClassName,Placeholder,OnChange<E>,InnerRef<E>,Value<V> {
  fieldType: F;
}

// defines props for a "Label" component
export interface LabelProps {
  name: string;
  label: string;
}

// defines props for an "input" element by extending the FieldProps interface
export interface InputProps
  extends FieldProps<HTMLInputElement,"input",ReactText> {
  type: "text" | "number" | "email" | "phone";
}

// defines props for an "textarea" element by extending the FieldProps interface
export interface TextAreaProps
  extends FieldProps<HTMLTextAreaElement,"textarea",string> {
  cols?: number;
  rows?: number;
}

// exporting React types for reusability
export type { ChangeEvent,FormEvent };

index.tsx

import * as React from "react";
import { render } from "react-dom";
import Form from "./components/Form";
import "uikit/dist/css/uikit.min.css";
import "./index.css";

render(<Form />,document.getElementById("root"));
,

我知道我回答这个问题真的晚了,但这就是我解决这个问题的方法。也许有一天这会对其他人有所帮助。

type InputElement = 'input' | 'textarea'

export type InputProps<E extends InputElement> = {
    multiline: E extends 'textarea' ? true : false
    /* rest of props */
}

const Component = React.forwardRef(function Component<E extends InputElement>(
    props: InputProps<E>,ref: React.Ref<HTMLElementTagNameMap[E] | null>,) {
,

这是一个棘手的问题,我认为唯一可行的方法是使用higher order componentfunction overloading

基本上,我们必须创建一个函数,该函数本身将根据所传递的参数而返回一种类型的组件或另一种类型的组件。

// Overload signature #1
function MakeInput(
  type: "textArea"
): React.ForwardRefExoticComponent<
  TextAreaProps & React.RefAttributes<HTMLTextAreaElement>
>;
// Overload signature #2
function MakeInput(
  type: "input"
): React.ForwardRefExoticComponent<
  InputProps & React.RefAttributes<HTMLInputElement>
>;
// Function declaration
function MakeInput(type: "textArea" | "input") {
  if (type === "textArea") {
    const ret = React.forwardRef<HTMLTextAreaElement,TextAreaProps>(
      (props,ref) => {
        return <TextArea {...props} ref={ref} />;
      }
    );
    return ret;
  } else {
    const ret = React.forwardRef<HTMLInputElement,InputProps>((props,ref) => {
      return <Input {...props} ref={ref} />;
    });
    return ret;
  }
}

然后,通过使用组件的“类型”调用高阶组件函数MakeInput()来实例化要渲染的组件类型:

export default function App() {
  const textAreaRef = React.useRef<HTMLTextAreaElement>(null);
  const inputRef = React.useRef<HTMLInputElement>(null);

  const MyTextArea = MakeInput("textArea");
  const MyInput = MakeInput("input");

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
      <MyTextArea value={"Foo"} ref={textAreaRef} />
      <MyInput value={"Bar"} ref={inputRef} />
    </div>
  );
}

现在,这可能会让人感到“不满意”,因为这大致相当于在此处进行条件检查,以查看基于type呈现的组件类型,该组件刚刚被抽象为一个函数。但是,您无法渲染一个神奇的<MyTextAreaOrInputComponent />并获得 both propsref属性的完整类型检查。为此,您必须归咎于React本身,因为ref道具(例如key以及其他一些道具)非常非常特殊,并且被React独特地对待,这正是{ {1}}首先。

但是,如果您考虑一下,实际上,您仍在进行所需的道具类型检查,只是您增加了调用React.forwardRef()以确定组件类型的额外步骤。所以不用写这个:

MakeInput()

您正在写这篇文章:

return <Component type="textArea" ref={textAreaRef} />

在这两种情况下,您在编写代码时都明确知道 {em> const MyComponent = MakeInput("textArea"); return <MyComponent ref={textAreaRef} /> type的值。由于ref的工作方式,就我所知,前一种情况是行不通的。但是后一种情况 是可能的,并且只需额外的步骤即可为您提供完全相同级别的类型检查。

https://codesandbox.io/s/nostalgic-pare-pqmfu?file=/src/App.tsx

注意:在上面的沙箱中玩耍,看看与React.forwardRef()相比<Input/>有一个额外的道具extraInputValue,高阶组件如何优雅地处理它。另请注意,使用任一有效字符串值调用<TextArea/>来创建组件都会导致预期的和正确的道具类型检查。

编辑:在类型检查方面,“魔术子弹”组件与使用HOC的另一种说明在功能上是相同的,因为在您的方案中,您同时知道MakeInput()和{{1 }}应该在预编译时代表,您可以按字面意义执行包含相同信息量的IIFE:

type

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-
参考1 参考2 解决方案 # 点击安装源 协议选择 http:// 路径填写 mirrors.aliyun.com/centos/8.3.2011/BaseOS/x86_64/os URL类型 软件库URL 其他路径 # 版本 7 mirrors.aliyun.com/centos/7/os/x86
报错1 [root@slave1 data_mocker]# kafka-console-consumer.sh --bootstrap-server slave1:9092 --topic topic_db [2023-12-19 18:31:12,770] WARN [Consumer clie
错误1 # 重写数据 hive (edu)&gt; insert overwrite table dwd_trade_cart_add_inc &gt; select data.id, &gt; data.user_id, &gt; data.course_id, &gt; date_format(
错误1 hive (edu)&gt; insert into huanhuan values(1,&#39;haoge&#39;); Query ID = root_20240110071417_fe1517ad-3607-41f4-bdcf-d00b98ac443e Total jobs = 1
报错1:执行到如下就不执行了,没有显示Successfully registered new MBean. [root@slave1 bin]# /usr/local/software/flume-1.9.0/bin/flume-ng agent -n a1 -c /usr/local/softwa
虚拟及没有启动任何服务器查看jps会显示jps,如果没有显示任何东西 [root@slave2 ~]# jps 9647 Jps 解决方案 # 进入/tmp查看 [root@slave1 dfs]# cd /tmp [root@slave1 tmp]# ll 总用量 48 drwxr-xr-x. 2
报错1 hive&gt; show databases; OK Failed with exception java.io.IOException:java.lang.RuntimeException: Error in configuring object Time taken: 0.474 se
报错1 [root@localhost ~]# vim -bash: vim: 未找到命令 安装vim yum -y install vim* # 查看是否安装成功 [root@hadoop01 hadoop]# rpm -qa |grep vim vim-X11-7.4.629-8.el7_9.x
修改hadoop配置 vi /usr/local/software/hadoop-2.9.2/etc/hadoop/yarn-site.xml # 添加如下 &lt;configuration&gt; &lt;property&gt; &lt;name&gt;yarn.nodemanager.res