🧹

JSX内での冗長な変数利用を整理する

2 min read 2

JSX内の変数利用は冗長になりやすい

例えばこんなコード、良く見るのではないでしょうか。

function Profile() {
  const [res] = useQuery({ query: GetCurrentUser })

  return (
    <div>
      <h1 tw="text-black font-bold text-3xl">Profile</h1>
      <div>
        {res.fetching && <Spinner global />}
        {res.error && <p>error</p>}
	{res.data?.currentUser && (
          <div>
            <Avatar name={res.data.currentUser.name} size="l" />
            <p>{res.data.currentUser.name}</p>
            <p>{res.data.currentUser.email}</p>
            <p>{res.data.currentUser.age}</p>
          </div>
        )}
      </div>
    </div>
  )
}

res.data.currentUserが何度も利用されていることに注目してください。明らかに冗長ですね。どうやって解決できるでしょうか。

勿論、コンポーネント分割するのは1つの方法です。UserCardみたいなcurrentUserと同一の型を受け取るコンポーネントに切り出すやつですね。ただ、切り出したくない場合はどうでしょうか。

変数代入をするのも手です。

function Profile() {
  const [res] = useQuery({ query: GetCurrentUser })

  if (res.fetching) return <Spinner />

  if (res.error || !res.data) return <p>error</p>

  const { currentUser } = res.data

  return (
    <div>
      <h1 tw="text-black font-bold text-3xl">Profile</h1>
      <div>
        {currentUser && (
          <div>
            <Avatar name={currentUser.name} size="l" />
            <p>{currentUser.name}</p>
            <p>{currentUser.email}</p>
            <p>{currentUser.age}</p>
          </div>
        )}
      </div>
    </div>
  )
}

良くなりました。しかし、res.fetchingやres.errorのハンドリングが変わってしまいました。こうしないとTSに怒られてしまいます。

better approarch: fromObject

そもそも本当に解決したかったことを整理しましょう。

{res.data?.currentUser && (
  <div>
    <Avatar name={res.data.currentUser.name} size="l" />
    <p>{res.data.currentUser.name}</p>
    <p>{res.data.currentUser.email}</p>
    <p>{res.data.currentUser.age}</p>
  </div>
)}

ここでcres.data.currentUserの冗長なコードを減らすには、「res.data.currentUserがあったら、res.data.currentUserを一時的に展開して利用しJSXを書く」ができれば良さそうです。

この一時的にオブジェクトを展開して利用する仕組みはたった数行のコードで実現できます。

export function fromObject<T extends Record<string, unknown>>(
  obj: T
): <R>(fn: (obj: T) => R) => R {
  return (fn) => fn(obj)
}

これを使うと該当部分は以下のようになります。

{res.data?.currentUser &&
  fromObject(res.data.currentUser)(({ name, email, age }) => (
    <div>
      <Avatar name={name} size="l" />
      <p>{name}</p>
      <p>{email}</p>
      <p>{age}</p>
    </div>
))}

とてもスッキリしましたね。型もちゃんと効いてます。

配列であればmapを使えば良いのですが、objectだと意外に綺麗にいかないので書いてみました。とてもシンプルな方法ですが、意外と今までみたことないのでもし他に良さそうな方法&意見あればお聞かせください。

Discussion

素敵なアイディアですね。
(cb: (obj: T) => any) => T
ではなく、
(cb: (obj: T) => any) => any
or
(cb: (obj: T) => ReactElement) => ReactElement
のような気がしますが、いかがでしょうか。
戻り値の型がTだと、ProfileがTを返すことになってしまいます。

function Profile() {...}の書き方だと大丈夫ですが、
const Profile: FC = () => {...}
のように型を明示的に指定するとTypeScriptのエラーになります。

汎用的に作るなら、fromObject.tsのようなTSファイルで、下記のように定義してあげると、TSXファイル以外でも使えるし、戻り値の型もちゃんと推測されます。

const fromObject = <T>(obj: T): (<R>(cb: (obj: T) => R) => R) => {
return (cb) => cb(obj);
};

export default fromObject;

ありがとうございます、仰るとおりでミスでした!
自分はanyを意図しておりましたが、cbのジェネリクス作ってあげたほうが親切かもですね。

ログインするとコメントできます