📑

【OSS開発】複雑化した条件付きレンダリングに疲弊した人を救いたい

2024/03/26に公開

はじめに

普段開発をしていてこのように思ったことはないでしょうか?
親要素のdivタグのレンダリングは条件付けて切り替えたいけど子要素はずっと表示していたいんだよなぁ~
つまりは以下のようなケースです。

return (
// divタグだけをレンダリングしたりしなかったりしたい
<div style="margin: 40px; background:#000000;"> 
  <h2>divタグが消えても俺は生き残る</h2>
  <p>ずっと表示していたい子要素</p>
  <ChildComponent />
</div>
)

まあ割と特殊なケースではありますが、上記の様なケースを実現するだけであれば条件付きレンダリングやCSSを駆使すれば十分可能です。

しかし、どうしても条件やスタイルが複雑になってしまったり、コードが無駄に冗長になってしまうなと悩んでいる方もいらっしゃるのではないかと思います。
筆者もそのうちの一人です。

そこで、そんな開発者(自分)の助けになれるライブラリCamo-tag🥷を作成したので紹介させてください。

もし最後まで読んで気に入っていただけましたら、ソースコードはGithubにOSSとして公開しておりますのでstarをもらえると大変励みになります🙇‍♂️

条件付きレンダリングについて

※ 条件付きレンダリングについての解説なんて今さらという感じかもしれないですが、Camo-tagの紹介にあたって必要な前置きになるのでご容赦ください。

条件付きレンダリング[1] は、ざっくり説明すると「様々な条件に基づいてコンポーネントに表示させる内容を変化させる手法」と言えるでしょう。

例えば、チェックボックスにチェックがついていれば選択済みだよ!と返して、チェックがついていないときには未選択だよ...と文章を返したいときには以下の様にすることで実現できます。

if (isPacked) {
  return <li className="item">{name} 選択済みだよ!</li>;
}
return <li className="item">未選択だよ...</li>;

または、条件式をJSXに含めることで以下の様に書くこともできます。

条件演算子
<li className="item">
  {isPacked ? (
     選択済みだよ!
  ) : (
    未選択だよ...
  )}
</li>

ただし後者の書き方(条件演算子)は複雑な条件分岐によりコンポーネントが汚染されることが多々あるためあまり推奨されていません。

もう一つよく使われるのが論理 AND 演算子 (&&) です。条件が真の場合に JSX をレンダリングし、それ以外の場合は何もレンダリングしないという場合にしばしば使用されます。

論理 AND 演算子 (&&)
<li className="item">
   {isPacked && '選択済みだよ!'}
</li>

Vueにおいてはv-ifv-showといった条件付きレンダリングに特化した機構が用意されています。

v-if
<li v-if="isPacked" className="item">
   選択済みだよ!
</li>
<li v-else className="item">
   未選択だよ...
</li>

条件付きレンダリングの限界

上記セクションの例だと条件付きレンダリングで十分実現できるのですが、以下のような条件があったときはどうでしょうか?

ある変数がfalseの時は親要素のみをレンダリングせず子要素はレンダリングしたい

コードベースで確認すると

<div style="background: #888888; margin: 80px">
    <h1>Home</h1>
    <div>
      <h2 style="color: red">Sub Title</h2>
    </div>
</div>

とあった時、

非レンダリング
<div style="background: #888888; margin: 80px"></div>

のみ非表示(非レンダリング)にして、以下のような子要素は表示(レンダリング)したいといったケースです。

レンダリング対象
<h1>Home</h1>
<div>
  <h2 style="color: red">Sub Title</h2>
</div>

条件付きレンダリングについてで紹介した手法で再現すると以下の様な方法が思いつくかもしれません。

{isPacked ?
 (<div style="background: #888888; margin: 80px">
    <h1>Home</h1>
    <div>
      <h2 style="color: red">Sub Title</h2>
    </div>
  </div>
) : (
<h1>Home</h1>
<div>
  <h2 style="color: red">Sub Title</h2>
</div>
)}

余りに煩雑ですね。では子要素をコンポーネントに切り分けたらどうでしょうか。

const Component = () => (
<h1>Home</h1>
<div>
  <h2 style="color: red">Sub Title</h2>
</div>
)

...
return (
{isPacked ?
 (<div style="background: #888888; margin: 80px">
    <Component />
  </div>
) : (
<Component />
)}
)

いずれにせよ条件分岐による複雑さや、この目的のためだけにコンポーネントを切り分けることに難色が示されると思います。

では次の例はどうでしょうか。

ある変数がfalseの時、ほとんどの要素はレンダリングしたくないが一部要素だけはレンダリングしたい

つまり、以下のようなコードがあった時、

<main className="main-container">
    <h2>HugaHuga</h2>
    <p>hogehoge</p>
    <p>NinNinjya<p>
    <Component />
</main>

ある変数がtrueの時は上記の全てをレンダリングし、falseの時は<Component />のみをレンダリングしたい。といったケースです。

これも、条件付きレンダリングについてで紹介した手法で以下の様に再現できます。

const ConditionalComponent = () => (
<>
    <h2>HugaHuga</h2>
    <p>hogehoge</p>
    <p>NinNinjya<p>
</>
)

~~~
return (
<main className="main-container">
  { isPacked && <ConditionalComponent /> }
  <Component />
</main>
)

このケースだと一見うまく書けている様に見えます。
しかし、mainタグもレンダリングの切替対象に入ってくると話は変わってきます。

return (
<>
  { isPacked ? 
    <main className="main-container">
        <ConditionalComponent />
        <Component />
    </main> 
    :
    <Component />
   }
</>
)

結局のところ、ある変数がfalseの時は親要素のみをレンダリングせず子要素はレンダリングしたいの場合と同じ問題点が発生してしまいます。

Camoの活用

Camo-tagは上述の条件付きレンダリングの限界をより簡潔に解決するのに非常に役に立つ機能を提供します。
例えば、ある変数がfalseの時は親要素のみをレンダリングせず子要素はレンダリングしたい の例はCamois-onlyという機能を用いることで以下の様に書くことができます。

<Camo as="div" is-only={!isPacked} style="background: #888888; margin: 80px">
    <h1>Home</h1>
    <div>
      <h2 style="color: red">Sub Title</h2>
    </div>
</Camo>

// isPacked === falseの時のレンダリング結果
// <h1>Home</h1>
// <div>
//   <h2 style="color: red">Sub Title</h2>
// </div>

また、ある変数がfalseの時、ほとんどの要素はレンダリングしたくないが一部要素だけはレンダリングしたい の例もCamois-allis-survivorという機能を用いることで簡潔に書くことができます。

<Camo as="main" is-all={!isPacked} className="main-container">
    <ConditionalComponent />
    <Component is-survivor={true} />
</Camo>

// isPacked === falseの時のレンダリング結果
// <Component />

このようにCamois-only,is-all,is-survivorといった機能を活用することで、特定のケースにおける条件付きレンダリングをより簡潔に書くことができるようになります。

is-only,is-all,is-survivorの機能についてのより詳細な説明はREADMEに記載していますので是非ご覧ください。

  • is-only
    • is-only属性にtrueを渡すことで、子要素はそのままにCamoタグだけを消すことができる
  • is-all & is-survivor
    • is-allはすべての子要素を削除する
    • is-survivorに囲まれた要素は、is-allから身を守る

https://github.com/nojiritakeshi/camo-tag

まとめ

Camoの独自性は、そのシンプルさと汎用性にあります。開発者は、Camoを用いるだけで複雑な条件付きレンダリングをより簡潔に書くことができるようになります。
このアプローチは、既存の条件付きレンダリングやコンポーネント設計をより柔軟にできるのではと考えています。

現在はReactとVueのみの対応となっていますが、需要があればReactNativeやHonoなどへの対応も随時行っていきます。

この記事を読んで少しでもいいなと感じていただけた方は是非GitHubのstarをいただけたらとても喜びます。

また、こういった機能が欲しいとか改善点などのご意見は大歓迎ですので、記事のコメントやIssueを立てていただけると幸いです。

https://github.com/nojiritakeshi/camo-tag

※完全に余談ですが、Camoの名前の由来はcamouflage(カモフラージュ)の略語であるCamoから来ています。迷彩の様に存在を自由自在に隠せるというイメージで命名しました。
迷彩柄の絵文字を探していたのですが見つからなかったので、

迷彩柄→隠れる→忍者→🥷

というトンデモ置換で忍者の絵文字をモチーフにしていますw🥷

脚注
  1. ReactとVueの公式ドキュメントによれば、Reactでは条件付きレンダーVueでは条件付きレンダリングと名付けられてますが、当該記事においては条件付きレンダリングに統一させていただきます。 ↩︎

Discussion