14
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

きゃりぱみゅの名曲にならえ!TypeScriptで書かれたFunctionComponentでchildrenPropを使う方法

Last updated at Posted at 2018-12-03

すみません、きゃりーぱみゅぱみゅは終盤まで出てきません🙇‍♂️

モチベーション🔥

ReactをTypeScriptで書いていたとき、
FunctionComponentでchildrenPropを使おうとしましたが、
書き方が結構ややこしかったので、アンサー何故そうなるのかを書いておこうと思った次第です!

TL; DR🍜

  • FunctionComponentは、外部から渡される引数にたいして、childrenPropを勝手に 合成 していた
  • 合成の理解には、まずTypeScriptの 交差型(Intersection Types) を知る必要があった

環境🤖

この記事では Reactv16.6 を使っています

記事に出てくるキーワード💬

FunctionComponent

  • 状態を持たない純粋関数コンポーネントのこと
  • StatelessComponent とか StatelessFunctionalComponent とか呼び名がある。正式名称はFunctionComponentぽい!

FunctionComponentとはこんなコンポーネントのことです。主にPresentationalComponentとして使われます。

const Hello = () => <p>Hello</p>

FuntionComponentはパフォーマンスの文脈で語られることが多いですが、
個人的には、状態を管理する場所を出来るだけ減らしたい、ということで使っています。

余談ですが、最近RFCが公開されたHooksもFunctionComponentをベースにしていますね。
Hooks.guideでHooksの実装例が披露されていたりします。コミュニティが盛り上がっている感じあります)

childrenProp

<div>
    <p>Hello</p> /* このpタグはdivタグに childrenProp として渡される */
<div>

詳しくはこの記事で説明しました

本題✍️

そもそもこの記事のきっかけになったのは、適当にウェブメディアっぽいものを作っていたときです。

具体例がないとわかりづらいので、ここからはQiitaの記事ページを例にして説明します。
スクリーンショット 2018-12-03 18.04.56.png

記事ページのURLですが、 https://qiita.com/${ユーザ名}/items/${記事Id} となっていますよね。

記事一覧ページからではなく、URLから記事ページに直アクセスされたとき、
URLに含まれる記事Idが不正だった(存在しない記事とか)場合、コンポーネントとしてどうハンドリングしようか
と考えていたのが出発点でした。


では、以下のコンポーネント構成がある前提で話をすすめます。

  • App.tsx:

    • React.Componentを継承したコンポーネント
    • getItemメソッドを使うと、記事をローカルあるいはリモートサーバーから取得できる
  • ItemPage:

    • FunctionComponent
    • App.tsxから記事を受け取って、表示する

はじめは、適当に以下のように書いていました。(React-Routerを使っています)

App.tsx

getItem = (id: string) => /* 記事を取得する処理 */

<BrowserRouter>
    <Switch>
    .
    . /* いろんなルーティング */
    .
        <Route
            exact
            path={"/items/:id"}
            render={({ match: { params }}: { match: { params: Object }}) => (
                <ItemPage item={getItem(params["id"])} />
            )}
        />
    </Switch>
</BrowserRouter>

今回の本題ではないのでReact-Routerの説明は省きますが、
このコードだとparams["id"]にアクセスすることで記事Idを取得できます。

ItemPageコンポーネントの item プロパティに、getItemメソッドで取得した記事を渡すというイメージです。

しかし

これだと、存在しない記事Idが渡ってきた時のハンドリングをItemPageコンポーネントに任せなければいけません。
つまり、ルーティングのミスをItemPageコンポーネントがカバーしなけれいけないという考え方になりますよね。

ItemPageに対して**「表示するものがない場合がある」という知識を持たせるのは微妙**ですね。

そこで、記事が存在するか確認して、
なければ404ページへと誘うラッパーコンポーネント(WithNotFound)をFunctionComponentとして定義しました。

ラッパーが引数にchildrenPropを受け取るため、以下のように記述してあげます。

WithNotFound.tsx
export const WithNotFound = ({
  targetPath, /* 記事Idが渡ってくる */
  predicate,  /* 記事Idを引数に受け取り、存在するか確認する関数 */
  children    /* このコンポーネントの内側に書いたコンポーネントは、childrenにわたってくる */
} : {
  targetPath: string;
  predicate: (targetPath: string) => boolean;
  children: React.ReactNode; /* 型定義が必要なので、なんでも受け取れる型にしておく */
}) => (predicate(targetPath) ? <>{children}</> : <NotFound />);

App.tsxも書き換えます。ItemPageコンポーネントをラッパーで包んであげます。

App.tsx
<BrowserRouter>
    <Switch>
    .
    . /* いろんなルーティング */
    .
        <Route
            exact
            path={"/items/:id"}
            render={({ match: { params }}: { match: { params: Object }}) => (
                <WithNotFound targetPath={params["id"]}
                    predicate={id => /* 記事Idを元に、記事があるか確認する処理 */}>

                    <ItemPage item={getItem(params["id"])} />

                </WithNotFound>
            )}
        />
    </Switch>
</BrowserRouter>

これで一旦完成です!、自分でchildrenPropを宣言しているのがダサい気がしたんですよね...

React.Componentを継承したコンポーネントであれば this.props.childrenでchildrenPropにアクセスできるのに!!

ここから本当の本題です!

ちょっと調べてみたら、React.FunctionComponentという出来合いの型でコンポーネントを作れば以下のようにスマートにかけることがわかりました✨

WithNotFound.tsx
export const WithNotFound: React.FunctionComponent<{
    targetPath: string;
    predicate: (targetPath: string) => boolean;
}> = ({
    children, /* FunctionComponentの中で実際に使いたいので、書く必要がある泣 */
    targetPath,
    predicate
}) => predicate(targetPath) ? <>{children}</> : <NotFound />;

あれ、childrenの型定義を書いていないけど、引数のchildrenが使える...?
コンパイラにもオコられないぞ...?

なんで?よくみると、React.ReactNodeという型が勝手についています。
スクリーンショット 2018-12-04 2.59.21.png

React.FunctionComponentをみてみましょう。どうなっているのやら!

index.d.ts
interface FunctionComponent<P = {}> {
    (props: P & { children?: ReactNode }, context?: any): ReactElement<any> | null;
    propTypes?: ValidationMap<P>;
    contextTypes?: ValidationMap<any>;
    defaultProps?: Partial<P>;
    displayName?: string;
}

props: P & { children?: ReactNode } に注目してください

どうやら、引数には Pというジェネリクス & { children?: ReactNode }というオブジェクト を受け取るそうです。

.
.
.

え、 &ってなんて説明すればいいんですか!🤷‍♂️

Pはわかります。上のコード例でいうところの

{
    targetPath: string;
    predicate: (targetPath: string) => boolean;
}

のことです。

ただ、、 **&**が...

で、調べてみたところ、&とは、TypeScriptの交差型のことだそうです。


説明するより例をみた方がわかりやすいので以下読んでみてください!

interface Hoge {
    foo: string
}

interface Fuga {
    bar: number
}

const hogeHuga: Hoge & Fuga = {foo: "ふ〜", bar: 99}

つまり、型を合成することができるんですね😲

なかなか使いどころが思いつきませんが、サーバーサイドにリクエストを送る時、どうしてもフォームに追加しなきゃいけないパラメータがあるケースなんかは、交差型が便利かもしれません!

例えば、どのデバイスからリクエストが送られたかも教えなきゃいけないケースとか(あるかな...?笑)

const person: Person = {name: "太郎"}

interface RequestInfo {
    device: string
}

const sendingPerson: Person & RequestInfo = {...person, ...{device: "iPhone"}}

例にもどります。props: P & { children?: ReactNode }に従うと、

props: { 
    targetPath: string; 
    predicate: (targetPath: string) => boolean
} & {
    children?: ReactNode
}

// つまり以下になる ↓

props: { 
    targetPath: string; 
    predicate: (targetPath: string) => boolean; 
    children?: ReactNode
} 

となることがわかりますね🎵
だから、childrenPropに型が勝手に与えられていたんですね〜

## さいごに🤚

きゃりーぱみゅぱみゅの『最&高』はつまり、最と高を合成していたんですね。歌って踊れてTypeScriptもできる才女です。

早&速、TypeScriptをもっと勉強したいと思います💪

14
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?