はじめに
本業でNext.js(React),TypeScriptを使用してて、React-Hook-Formを実装する際にrefを含むregister
を自作コンポーネントを渡した時に、
Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
とエラーが出てのでその対処法についてのアウトプットです。
また、子コンポーネントにinputの属性含むpropsの型定義が冗長になってたのでrefを含むComponentPropsWithoutRefを使って対処・リファクタリングも行いました。
refについての説明は省いております。
対処したコード
今回は自作したlabel付きのInputコンポーネントに対してバリデーションを実装したので、labelの型を含ませた子コンポーネントになっております。
type LabelProps = {
label: string;
}
type ChildProps = ComponentPropsWithoutRef<'input'> & LabelProps;
const ChildInput = React.forwardRef<HTMLInputElement, ChildProps>(
({ label, ...props }, ref) => {
return (
<div>
<p>{label}</p>
<input placeholder={label} {...props} ref={ref} />
</div>
);
});
ChildInput.displayName = "ChildInput"
該当エラー
親コンポーネント
import React, { FC } from "react";
import {useForm, SubmitHandler} from "react-hook-from";
import ChildInput from "./ChildInut";
const Parent: FC = () => {
const {
handleSubmit,
register,
formState: { errors },
} = useForm<{parent: string}>({
defaultValues: { parent: ''},
});
const onSubmit: SubmitHandler = (data) => {
console.log(data)
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<ChildInput
{...register('parent', { required: true })}
label='子供にRefを投げる!!'
/>
<button>送信</button>
</form>
)
export default Parent
子コンポーネント
import React, { FC } from "react";
type ChildProps = {
name: string;
type: string;
value: string;
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
label: string;
}
const ChildInput: FC<ChildProps> = ({ label, name, type, value, onChange }) => {
return (
<div>
<p>{label}</p>
<input
name={name}
type={type}
value={value}
onChange={onChange}
placeholder={label}
/>
</div>
);
};
export default ChildInput;
以上の子コンポーネントにrefを含むregister
をそのまま渡すと関数コンポーネントにrefを渡すことができないとエラーが表示されます。
なのでReact.fowardRefを使ってねと言っているので自作のコンポーネントに対してReact.fowardRefを使ってみましょう。
また、各HTML要素の属性とrefの型を含むComponentPropsWithoutRefを使ってスッキリさせていきましょう。
解決
- import React, { FC } from "react";
+ import React, { forwardRef, ComponentPropsWithoutRef } from "react";
- type ChildProps = {
- name: string;
- type: string;
- value: string;
- onChange: (e: ChangeEvent<HTMLInputElement>) => void;
- label: string;
- }
+ type LabelProps = {
+ label: string;
+ }
+ type ChildProps = ComponentPropsWithoutRef<'input'> & LabelProps;
- const ChildInput: FC<ChildProps> = ({ label, ...props }) => {
+ const ChildInput = forwardRef<HTMLInputElement, ChildProps>(
+ ({ label, ...props }, ref) => {
return (
<div>
<p>{label}</p>
- <input
- name={name}
- type={type}
- value={value}
- onChange={onChange}
- placeholder={label}
- />
+ <input placeholder={label} {...props} ref={ref} />
</div>
);
- }
+ });
+ ChildInput.displayName = "ChildInput"
export default ChildInput;
詳細
・ forwardRefとは
~ Reactのドキュメントから引用 ~
コンポーネントが ref を受け取って、それをさらに下層の子に渡せる(つまり、ref を “転送” できる)ようになります。
ドキュメントにもあるように、自作したコンポーネントのDOM要素にrefを渡す時に使います。
・ ComponentPropsWithoutRefについて
ref要素を含むコンポーネントのpropsの型を作成することが出来る。
同様にComponentPropsWithRefでも使用できますが、違いとしてはComponentPropsWithoutRefと比べてクラスコンポーネントにも対応しているそうです。
以下はReactの型定義から参照したものです。
ComponentPropsWithoutRef/ComponentPropsWithRef 型の違い
type ComponentPropsWithRef<T extends ElementType> =
T extends ComponentClass<infer P>
? PropsWithoutRef<P> & RefAttributes<InstanceType<T>>
: PropsWithRef<ComponentProps<T>>;
type ComponentPropsWithoutRef<T extends ElementType> =
PropsWithoutRef<ComponentProps<T>>;
・ ESLint Error: Component definition is missing display name eslint(react/display-name)
+ ChildInput.displayName = "ChildInput"
ここの箇所については、ESLintにデバック時の関数名が無いとエラーが出たの追加しました。
Component definition is missing display name eslint(react/display-name)
おそらくforwardRefで返す関数が無名関数になっている影響ですね。
まとめ
-
refをコンポーネントに渡す時は、fowardRefでコンポーネントをラップすることでコンポーネント内のDOMにrefを渡すことができる。
-
TypeScriptを使用している場合は、ComponentPropsWithRefあるいはComponentPropsWithoutRefでrefと該当のHTML属性を含んだpropsを子コンポーネントが受け取ることが出来る。
ComponentPropsWithoutRefでrefと各HTML要素にも対応してたので感動しました泣
おかげでコードがスッキリになりました✨
引き続きTypeScriptとReactについて学習していきます!!