はじめに
基本的にReact + TypeScriptでフロントの開発をしているんですが、実際にコードを書いている時に気をつけていること、便利な書き方として知っておくと得をするReactコンポーネントの書き方を紹介します。
Propsが多くなりすぎたら
やたらpropsが多くなってしまうことありませんか?しかも同じような名称ばっかりを何回も書くことになるという。そうゆうときはできる限りショートハンドで書きましょう。
return (
<SampleComponent
type={user.type}
name={user.name}
email={user.email}
image={user.image}
/>
)
Componentに全てのPropsを渡す場合は下記のようにするとコード量がだいぶ減りますね。
return (
<SampleComponent {...user} />
)
Component側に渡すのが全てではなくいくつか決められたものの場合はこちらです。
const {type, name, email, image} = user
return (
<SampleComponent {...{type, name, email, image}} />
)
とはいえ、ショートハンドを覚えるのも大事ですが、Propsが多すぎる場合にはそもそComponentの分割を検討した方が良いでしょう。
React.FCの拡張
ComponentをCSS modulesやCSS in JSで作る際に className
をPropsで渡せるようにしたいですよね?もちろん下記のようにすればclassNameを渡せるんですけど、いちいち数十個、数百個もあるComponentに対して型定義するのは正直めんどくさい。
type Props = {
className?: string
}
export const Component: FC<Props> = ({ children, className }) => {
return <div className={classnames(style.heading, className)}>{children}</div>
}
こちらの React.FC 型を拡張する - Qiita を参考にさせてもらって、上記問題も無事解決!
ざっくりやり方を説明すると別の型を用意して、declare module に新しい型を追加するだけ!
プロジェクトの任意のディレクトリに type.ts
を作ってReact.FCに className
を追加した FCX
を定義します。
declare module 'react' {
type FCX<P = {}> = FunctionComponent<P & { className?: string }>
}
もちろん VFCも同じように作ることができます。
ComponentのタグをPropsで渡すようにする
ページの見出しで使う用にComponentをh1で作ったはいいけど、ここはh2の方が適切じゃね?ってことありませんか。「あー、コンポーネントのタグ変えられたら楽なのに」ってなりますよね。それ、実はできます。
export const Heading: FCX<Props> = ({ children }) => {
return <h1 className={classnames(style.heading, className)}>{children}</h1>
}
propsに渡すことでできます。注意点としては Component
のように最初を大文字にしてください。というのもReactでコンポーネントを書くときは <Component></Component>
のように最初の文字は大文字ですよね。それと同じです。
型はもちろん string
でもいけるんですが、存在しないタグはダメなので、下記のように型ガードさせるようにしましょう。
type Props = {
Component?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
}
export const Heading: FCX<Props> = ({ children, Component = 'h1' }) => {
return <Component className={classnames(style.heading, className)}>{children}</Component>
}
Function as children
childrenにpropsを渡すことができるって知ってました?例えば下記のようにボタンをクリックしたらinput要素が現れるComponentを作ることもできます。
比較的同じような動作をしてかつ、同一ページに複数存在する可能性があるようなComponentに向いている気がしますね。例えば、モーダルとかドロップダウンメニューとか。
type Props = {
children: (collapsed: boolean, toggleCollapse: () => void) => ReactNode;
};
export const CollapsibleComponent: FC<Props> = ({ children }) => {
const [collapsed, setCollapsed] = useState(false);
const toggleCollapse = () => {
setCollapsed(!collapsed);
};
return <div>{children(collapsed, toggleCollapse)}</div>;
};
export const HogeComponent = () => {
return (
<div>
// ・・・
<CollapsibleComponent>
{(collapsed, toggleCollapse) => {
if (collapsed) {
return (
<div>
<input type="text" value="React 太郎" />
<button type="button" onClick={toggleCollapse}>
閉じる
</button>
</div>
);
}
return (
<div>
React 太郎
<button type="button" onClick={toggleCollapse}>
開く
</button>
</div>
);
}}
</CollapsibleComponent>
// ・・・
</div>
)
}
Componentのジェネリクスの書き方
Componentに対してジェネリクス書けたらいいのにって時ありません?実は書けます。
今回は render props を使ったComponentでジェネリクスを使ってみましょう。
render propsは受け取ったpropsを描画に使うことができます。ほとんどFunction as Childrenと同じなんですけどね。以前はApollo Clientでもこのrender propsを押していたような気もします。今はhooks推しですね。
早速ですがReact Queryで取得したデータを render
に渡して描画させてみます。まず全体像はこちら。
type Props<T> = {
queryKey: string
render: (queryResult: UseQueryResult<T>) => JSX.Element
}
export const Fetch = <T,>({ render, queryKey }: Props<T>): JSX.Element => {
const query = useQuery(queryKey, async (): Promise<T> => api.get(queryKey), { enabled: !!queryKey })
if (query.isLoading) {
return <h2>Loading...</h2>
}
return render(query)
}
export const Home: React.FC = () => {
return (
<Fetch<Item[]>
queryKey="items"
render={({ data }) => {
return (
<div>
{data?.map((e) => {
return <div key={e.id}>{e.title}</div>
})}
</div>
)
}}
/>
)
}
FetchコンポーネントではrenderとqueryKeyを受け取ってrenderにReact Queryで取得したqueryResultを渡しています。あと、 取得するデータによって型が違うので Fetch<T>
のようにジェネリクスになっていますね。
で、ここで注意点なんですが、Componentでジェネリクスをアロー関数で使いたい場合は <T,>
のように ,
を入れるようにしてください。それでできます。自分もこれは知らなかった。,
をつけていないとReactプロジェクトだと「終了タグどうした!!!」って怒られますww
また、loading中は Loading...
と表示させて終わったら return render(queryResult)
としています。
type Props<T> = {
queryKey: string
render: (queryResult: UseQueryResult<T>) => JSX.Element
}
export const Fetch = <T,>({ render, queryKey }: Props<T>): JSX.Element => {
const query = useQuery(queryKey, async (): Promise<T> => api.get(queryKey), { enabled: !!queryKey })
if (query.isLoading) {
return <h2>Loading...</h2>
}
return render(query)
}
// こっちの書き方でもOK!
export function Fetch<T>({ render, queryKey }: Props<T>): JSX.Element {
const queryResult = useQuery(queryKey, async (): Promise<T> => api.get(queryKey), { enabled: !!queryKey })
if (queryResult.isLoading) {
return <h2>Loading...</h2>
}
return render(queryResult)
}
次にFetchコンポーネントの使用部分をみてみます。まず目に着くのは <Fetch<Item[]> />
ですよね。なんとも気持ち悪い感じ笑
実はこれがComponentのジェネリクスの書き方なんです。render内で受け取ったqueryResultからdataを取り出せていますね。
export const Home: React.FC = () => {
return (
<Fetch<Item[]>
queryKey="items"
render={({ data }) => {
return (
<div>
{data?.map((e) => {
return <div key={e.id}>{e.title}</div>
})}
</div>
)
}}
/>
)
}
hooksの登場であまり活躍の場面がないrender propsですが、hooksと組み合わせることで使える場面は出てきそうですね。というかこのパターンだと毎回 Loading
しなくていいしめっちゃ良さげです。
また、Componentのジェネリクスも覚えておくと便利ですね。例えば取得したデータを表示させるTableがあったとして、ジェネリクスがあれば汎用的に使えそうです。
オブジェクトリテラルをComponentに活用する
何でもかんでもif文とかswitch文で出しわけしてませんか?オブジェクトリテラルを活用するとスッキリ宣言的に書くことができますよ。
まずは条件分岐パターンです。よく見るやつですが、typeが追加されると変更箇所が ACCOUNT_TYPE
と switch
に追加する必要が出てきますね
const SampleComponent = (account) => {
const { type } = account;
const ACCOUNT_TYPE = {
ADMIN: "ADMIN",
OPERATOR: "OPERATOR",
VISITOR: "VISITOR",
};
switch (type) {
case ACCOUNT_TYPE.ADMIN:
return <Admin />;
case ACCOUNT_TYPE.OPERATOR:
return <Operator />;
case ACCOUNT_TYPE.VISITOR:
return <Visitor />;
default:
return null;
}
};
オブジェクトリテラルを使うとこんな感じです。めっちゃスッキリ!!変更箇所もcomponentsに追加するだけ!
const SampleComponent = (account) => {
const {type} = account
const components = {
ADMIN: Admin,
OPERATOR: Operator,
VISITOR: Visitor
};
const Component = components[type];
return <Component />;
}
おわりに
はじめてのアドカレだったんで色々調べながら記事にしました。ReactのComponentは関数なので JavaScript、TypeScriptでできることはほぼほぼできるんですよね。
また、結構知らないこともあってしかも早速プロジェクトに実践できそうです。