🦖

Reactの"要素の型"、それぞれの特性理解していますか?

2023/06/15に公開2

はじめに

普段React・Next.jsを用いた開発に際して、UIコンポーネントを作成する際にReact.FCやJSX.Element、ReactNodeなどの"要素やコンポーネントに関する型"を使用する場面が多々あるかなと思います。

昨今のReactコンポーネント開発において、実際には要素やコンポーネントの型を「何を使うか」をあまり意識しないでもちゃんと開発できてしまいます。とはいえ、それぞれの型には微妙に異なる特徴や特性がありますので、今回はよく見かける以下の四つの型について、それぞれの違いや特性を紹介していきたいと思います。

  1. JSX.Element
  2. ReactElement
  3. ReactNode
  4. React.FC

ReactElement

ReactElementはReactのコンポーネント(のインスタンス)を表現する最も基本的な型です。
具体的な型の定義は以下のようになっており、keyやpropsなどのプロパティを持ちます。

interface ReactElement<
	P = any, T extends string
	| JSXElementConstructor<any> = string
	| JSXElementConstructor<any>
> {
	type: T;
	props: P;
	key: Key | null;
}

https://github.com/microsoft/TypeScript/blob/aa44cd80049a8dec1fbd7afe8a92f617a3b3537e/tests/lib/react18/react18.d.ts#L141-

JSX.Element

JSX.ElementはJSXの結果の型として表現できます。
JSXはグローバルネームスペースと呼ばれるもので、グローバルスコープにあるオブジェクトのようになっています。

この名前空間には型を入れることができ、Elementはその型の1つです。

関数コンポーネントの返却値がJSXとしてレンダリング可能であることを型レベルで指定するのによく使います。コンポーネントの返却値の型としてJSX.Elementを指定した場合、nullを返却することができません

JSX.ElementReactElement型を拡張したもので、実際には以下のような実装になっています。

declare global { 
	namespace JSX {
		interface Element extends React.ReactElement<any,  any> { } 
		// ...
	}
}
...   

ReactNode

ReactNodeは、Reactコンポーネントがレンダリングできるものを表現する型であり、ReactのネームスペースにあるReact要素の型のスーパーセットです。これはReactElementだけでなく、文字列や数値(これらはテキストノードとしてレンダリングされる)、およびそれらの配列(子要素のリストとしてレンダリングされる)も含みます。

TypeScript5.1以降では、コンポーネントの返却値としてReact.ReactNodeが使用できるようになるみたいです。

ReactNodeは以下のものを含むより広範な型を表現することができます。

  • ReactChild(ReactElementまたはReactText)
  • ReactFragment(key付きまたはkey無しのReactNodeArray)
  • ReactPortal
  • string
  • number
  • boolean
  • null
  • undefined

したがって、ReactNodeはpropsのchildrenの型として一般的に使用されます。

ReactNodeの型定義を覗いてみると、以下のように定義されています。

type.ts
interface DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_REACT_NODES {}
type ReactNode =
	| ReactElement
	| string
	| number
	| ReactFragment
	| ReactPortal
	| boolean
	| null
	| undefined
	| DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_REACT_NODES[keyof DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_REACT_NODES];

https://github.com/microsoft/TypeScript/blob/aa44cd80049a8dec1fbd7afe8a92f617a3b3537e/tests/lib/react18/react18.d.ts#L231-

React.FC

React.FCReact.FunctionComponentの型の省略バージョンで、propsの型をジェネリクスで指定することができます。また、ReactElementを返却値の型としています。
さらにReact 18以降では、暗黙的に含まれていたchidrenが排除されたため、必要であれば自前でpropsの型にchildrenを含めなければなりません。

https://github.com/microsoft/TypeScript/blob/aa44cd80049a8dec1fbd7afe8a92f617a3b3537e/tests/lib/react18/react18.d.ts#L518

React childrenを何の型にするか問題

割とよく見かける議論で「childrenを何の型で扱うか」という問題があります。
おそらく候補として上がるのは、大概React.FCを除く三つの型かなと思います。
上記のそれぞれの性質をみるとなんとなく見えてくると思いますが、ReactNodeが最適かなと思います。

childrenはslottableであるため、DOM要素が来ることもあればテキストノードのような形式のオブジェクトになることもあります。

例えば、ReactElementやJSX.Elementにしてしまうと、以下のようなケース(テキストノードをchildrenとして渡す)で型エラーが発生します。


const Sample = ({ children }: { children: React.ReactElement }) => {
	return <div>{children}</div>;
}

export const Sample2 = (): JSX.Element => {
  return (
    <div>
      // error: Sample コンポーネントには、テキストを子要素として指定できません。
      // JSX のテキストには型が含まれていますが、children の予期された型は ReactElement> | undefined です。
      <Sample>sample text</Sample>
    </div>
  );
};

まとめ

冒頭でも記述したのですが、やはり最近のReactコンポーネント開発で要素やコンポーネントの型はあまり理解していなくても使用できてしまいますね。

ただ、類似した型の特性や違いを理解することは健全な型表現につながります。
絶対に知っていて損はないと思います。

それでは、良いReactライフを!🥳


Discussion

Wataru ItoWataru Ito

気になっていたことがスッキリしました!ありがとうございます

shuhei aoyamashuhei aoyama

Type definitions for React 18.2 だとFCの戻り値はReactNodeに変わっていますね。