🔄

テンプレートリテラルで掴むReactの動き方

2023/01/25に公開

Reactに入門すると、HTMLの書き方の次あたりに「状態の管理やアップデート方法」を学ぶと思います。これにより簡単なアプリケーションを作成することができるようになりますが、少し上達すると「状態に基づいた新しい値を作成したい」ニーズが生じるはずです。
そこで同時期に学ぶuseEffectを使い、状態に基づいた新しい値の作成を試すと思います。

一方You Might Not Need an Effectという記事があり、「レンダリング時のデータ変換にエフェクトは必要ないことが多い」としています。この辺りで混乱が生じる方もいらっしゃるのではないでしょうか。「どのような方法で状態に基づいた新しい値を作成/更新するのか」という疑問です。

これを受け、「素のJavascriptでReactを真似ればシンプルに概観が掴めるのではないか」と筆者は考えました。具体的にはelement.innerHTMLとテンプレートリテラルを用いてjsで動的にHTMLを作成する方法です。「Reactは関数が動いている」という言説について、単純化して捉える試みとなっています。

本記事ではJavascriptのelement.innerHTMLとテンプレートリテラルを用い、最終的にReactのコードに変換します。最後に「Reactってただの関数じゃん!」「Reactってシンプル〜!」となってくれれば嬉しいです。

(単純化を目的にしたたため曖昧な部分も多いと思いますが、誤りがあればぜひご指摘いただけると幸いです。)

本記事の対象

  • React初学者
  • Reactの考え方がしっくりきていない方

ゴール

  • Reactのメンタルモデルをざっくりと掴む
  • 不要なstate、useEffectが避けられるようになる

Step0. まえおき

念の為、前提となるelement.innerHTMLとはテンプレートリテラルを抑えておきます。
element.innerHTMLとは、HTMLエレメントの子要素として含まれるHTML文字列を取得または設定するプロパティです。document.getElementById()などでで取得したHTMLエレメントに対して、以下のようにHTML文字列を設定することができます。

//例
const element = document.getElementById("app")
element.innerHTML = "<h1>タイトル</h1><p>これはサンプルのテキストです。</p>";

テンプレートリテラルを使用すると、文字列や変数を操作し効率的な文字列生成が可能です。テンプレートリテラルとその内部で${変数や式}を使うことで、文字列内に変数の値を埋め込んだり文字列の連結を行うことができます。

const name = "sagawa";
console.log(`Hello, ${name}!`); // Hello, sagawa!

Element.innerHTMLとテンプレートリテラルを合わせて使用することで、要素の内部を変数に応じて変更することができます。

const element = document.getElementById("app")

const title = 'タイトル';
const text = 'これはサンプルのテキストです。';

const htmlString = `<h1>${title}</h1><p>${text}</p>`;
element.innerHTML = htmlString;

Step1. WebページをJavaScriptで作る

作業を始めるにあたりHTMLとCSSのファイル(CodeSandbox参照)を作成します。
HTMLファイルには<div id="app"></div>とIDを振っておきます。
これは後ほどJSで要素を取得するためです。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>element.innerHTMLで掴むReactの動き方</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <div id="app">
      <div class="layout">
        <h1>element.innerHTMLとテンプレートリテラルで掴むReactの動き方</h1>
        <p>こんにちは世界。</p>
        <ul class="list">
          <li class="listItem">
            <h2>サムネイル</h2>
            <p>サンプルテキストサンプルテキストサンプルテキスト</p>
          </li>
          <li>
            <h2>サムネイル</h2>
            <p>サンプルテキストサンプルテキストサンプルテキスト</p>
          </li>
          <li>
            <h2>サムネイル</h2>
            <p>サンプルテキストサンプルテキストサンプルテキスト</p>
          </li>
          <li>
            <h2>サムネイル</h2>
            <p>サンプルテキストサンプルテキストサンプルテキスト</p>
          </li>
          <li>
            <h2>サムネイル</h2>
            <p>サンプルテキストサンプルテキストサンプルテキスト</p>
          </li>
          <li>
            <h2>サムネイル</h2>
            <p>サンプルテキストサンプルテキストサンプルテキスト</p>
          </li>
        </ul>
      </div>
    </div>
    <script src="main.js" defer></script>
  </body>
</html>

雛形が用意できましたので、ここからReactに近づけていきます。
まずは、<div id="app"></div>を取得して、<div id="app"></div>の中身を変更してみます。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>element.innerHTMLで掴むReactの動き方</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <div id="app"></div>
    <script src="main.js" defer></script>
  </body>
</html>

//要素の取得
const target = document.getElementById("app");

//HTMLの挿入
target.innerHTML = `
<div class="layout">
<h1>element.innerHTMLとテンプレートリテラルで掴むReactの動き方</h1>
<p>こんにちは世界。</p>
<ul class="list">
  <li class="listItem">
    <h2>サムネイル</h2>
    <p>サンプルテキストサンプルテキストサンプルテキスト</p>
  </li>
  <li>
    <h2>サムネイル</h2>
    <p>サンプルテキストサンプルテキストサンプルテキスト</p>
  </li>
  <li>
    <h2>サムネイル</h2>
    <p>サンプルテキストサンプルテキストサンプルテキスト</p>
  </li>
  <li>
    <h2>サムネイル</h2>
    <p>サンプルテキストサンプルテキストサンプルテキスト</p>
  </li>
  <li>
    <h2>サムネイル</h2>
    <p>サンプルテキストサンプルテキストサンプルテキスト</p>
  </li>
  <li>
    <h2>サムネイル</h2>
    <p>サンプルテキストサンプルテキストサンプルテキスト</p>
  </li>
</ul>
</div>
`;

少し改良し、Reactに近づけるために関数から値を返すようにします。
Layout()から文字列が返され、target.innerHTMLに埋め込まれるようになりました。

//要素の取得
const target = document.getElementById("app");

const Layout = () => {
  return `
  <div class="layout">
    <h1>element.innerHTMLとテンプレートリテラルで掴むReactの動き方</h1>
    <ul class="list">
      <li class="listItem">
        <h2>見出し</h2>
        <p>サンプルテキストサンプルテキストサンプルテキスト</p>
      </li>
      <li>
        <h2>見出し</h2>
        <p>サンプルテキストサンプルテキストサンプルテキスト</p>
      </li>
      <li>
        <h2>見出し</h2>
        <p>サンプルテキストサンプルテキストサンプルテキスト</p>
      </li>
      <li>
        <h2>見出し</h2>
        <p>サンプルテキストサンプルテキストサンプルテキスト</p>
      </li>
      <li>
        <h2>見出し</h2>
        <p>サンプルテキストサンプルテキストサンプルテキスト</p>
      </li>
      <li>
        <h2>見出し</h2>
        <p>サンプルテキストサンプルテキストサンプルテキスト</p>
      </li>
    </ul>
  </div>`;
};

target.innerHTML = Layout();

Step2. パーツに分けてみる

テンプレートリテラルはJavaScript式を実行しその結果を埋め込むことができます。したがって、先ほどのLayoutのような関数を作りそれを実行すれば文字列を埋め込みができます。

//要素の取得
const target = document.getElementById("app");

//HTMLの挿入
const Title = () => {
  return `
    <h1>element.innerHTMLとテンプレートリテラルで掴むReactの動き方</h1>
  `;
};

const Layout = () => {
  return `
  <div class="layout">
    ${Title()}
    <ul class="list">
    ~~省略~~
    </ul>
  </div>`;
};

target.innerHTML = Layout();

次にカードのリストも同様に切り分け…

const List = () => {
  return `
  <ul class="list">
    <li class="listItem">
      <h2>見出し</h2>
      <p>サンプルテキストサンプルテキストサンプルテキスト</p>
    </li>
    <li class="listItem">
      <h2>見出し</h2>
      <p>サンプルテキストサンプルテキストサンプルテキスト</p>
    </li>
    <li class="listItem">
      <h2>見出し</h2>
      <p>サンプルテキストサンプルテキストサンプルテキスト</p>
    </li>
    <li class="listItem">
      <h2>見出し</h2>
      <p>サンプルテキストサンプルテキストサンプルテキスト</p>
    </li>
    <li class="listItem">
      <h2>見出し</h2>
      <p>サンプルテキストサンプルテキストサンプルテキスト</p>
    </li>
    <li class="listItem">
      <h2>見出し</h2>
      <p>サンプルテキストサンプルテキストサンプルテキスト</p>
    </li>
  </ul>
  `;
};

const Layout = () => {
  return `
  <div class="layout">
    ${Title()}
    ${List()}
  </div>`;
};

このまま内部のアイテムも関数にしてみます。

const ListItem = () => {
  return `
  <li>
    <h2>見出し</h2>
    <p>サンプルテキストサンプルテキストサンプルテキスト</p>
  </li>
  `;
};

const List = () => {
  return `
  <ul class="list">
    ${ListItem()}
    ${ListItem()}
    ${ListItem()}
    ${ListItem()}
    ${ListItem()}
    ${ListItem()}
  </ul>
  `;
};

しかし、このままでは少し冗長であり見出しや本文を変えることができません。したがって、ListItemを改良し引数から内部の値を設定できるようにします。

Step3. 引数を与えてみる

ListItemにオブジェクト{title: string, text: string}を受け取るよう引数を設定し、List内部でListItemを呼び出す際に引数を与えるようにしました。

// propps : {title: string, text: string}
const ListItem = (props) => {
  return `
  <li>
    <h2>${props.title}</h2>
    <p>${props.text}</p>
  </li>
  `;
};

const List = () => {
  return `
  <ul class="list">
    ${ListItem({ title: "タイトル1", text: "テキスト1" })}
    ${ListItem({ title: "タイトル2", text: "テキスト2" })}
    ${ListItem({ title: "タイトル3", text: "テキスト3" })}
    ${ListItem({ title: "タイトル4", text: "テキスト4" })}
    ${ListItem({ title: "タイトル5", text: "テキスト5" })}
    ${ListItem({ title: "タイトル6", text: "テキスト6" })}
  </ul>
  `;
};

まだ少し冗長ですのでデータを逐一渡すのではなくて配列化してみます。List内部にitemListを用意しmapでListItemを呼び出すようにします。
.mapは配列から新しい配列を返すものです。あくまで配列が返されるので.json("")を用いて文字列に変換しています。

const List = () => {
  const itemList = [
    { title: "タイトル1", text: "テキスト1" },
    { title: "タイトル2", text: "テキスト2" },
    { title: "タイトル3", text: "テキスト3" },
    { title: "タイトル4", text: "テキスト4" },
    { title: "タイトル5", text: "テキスト5" },
    { title: "タイトル6", text: "テキスト6" }
  ];

  return `
  <ul class="list">
    ${itemList.map((item) => ListItem(item)).join("")}
  </ul>
  `;
};

Step4. 引数に応じた条件分岐を行う

だいぶ良い感じに分割することができてきました。ここでちょっとアプリに近づけてフィルター機能っぽいものもつけてみましょう。オブジェクトにbooleanのflagプロパティを追加し、List内部でフィルターをかけてみます.

//要素の取得
const target = document.getElementById("app");

//HTMLの挿入
const Title = () => {
  return `
    <h1>element.innerHTMLとテンプレートリテラルで掴むReactの動き方</h1>
    <p>こんにちは世界。</p>
  `;
};

// propps : {title: string, text: string}
const ListItem = (props) => {
  return `
  <li>
    <h2>${props.title}</h2>
    <p>${props.text}</p>
  </li>
  `;
};

const List = () => {
  const itemList = [
    { title: "タイトル1", text: "テキスト1", flag: true },
    { title: "タイトル2", text: "テキスト2", flag: false },
    { title: "タイトル3", text: "テキスト3", flag: true },
    { title: "タイトル4", text: "テキスト4", flag: false },
    { title: "タイトル5", text: "テキスト5", flag: true },
    { title: "タイトル6", text: "テキスト6", flag: false },
  ];

  //itemListをフィルタリングするかどうかのフラグ
  const filterlingFlag = true;

  //flagによってフィルタリングする
  const filteredItemList = itemList.filter((item) => {
    return item.flag === filterlingFlag;
  });

  return `
  <ul class="list">
    ${filteredItemList.map((item) => ListItem(item)).join("")}
  </ul>
  <span>${filterlingFlag ? "true" : "false"}<span>
  `;
};

const Layout = () => {
  return `
  <div class="layout">
    ${Title()}
    ${List()}
  </div>`;
};

target.innerHTML = Layout();

filterlingFlagを用意し新しくfilteredListを作成しました。filterは元の配列から条件に合致した要素のみを返し新しい配列を作る関数です。これで少々強引ではありますがフィルタリング機能の完成です。

Step5. Reactに置き換える

それではこれまで作成したコードをReactに置き換えてみましょう。

const Title = () => {
  return (
    <>
      <h1>element.innerHTMLとテンプレートリテラルで掴むReactの動き方</h1>
      <p>こんにちは世界。</p>
    </>
  );
};

const ListItem = (props: { title: string; text: string }) => {
  return (
    <li>
      <h2>{props.title}</h2>
      <p>{props.text}</p>
    </li>
  );
};

const List = () => {
  const itemList = [
    { title: "タイトル1", text: "テキスト1", flag: true },
    { title: "タイトル2", text: "テキスト2", flag: false },
    { title: "タイトル3", text: "テキスト3", flag: true },
    { title: "タイトル4", text: "テキスト4", flag: false },
    { title: "タイトル5", text: "テキスト5", flag: true },
    { title: "タイトル6", text: "テキスト6", flag: false }
  ];
  //itemListをフィルタリングするかどうかのフラグ
  const filterlingFlag = true;
  //flagによってフィルタリングする
  const filteredItemList = itemList.filter((item) => {
    return item.flag === filterlingFlag;
  });

  return (
    <>
      <ul className="list">
        {filteredItemList.map((item) => (
          <ListItem {...item} />
        ))}
      </ul>
      <span>{filterlingFlag ? "true" : "false"}</span>
    </>
  );
};

const Layout = () => {
  return (
    <div className="layout">
      <Title />
      <List />
    </div>
  );
};

function App() {
  return (
    <div className="App">
      <Layout />
    </div>
  );
}

export default App;

テンプレートリテラルやHTMLタグがJSXになり、呼び出し方が関数からJSX特有の呼び出し方になりました(Layout()<Layout>)。しかし、基本的なコード構成は変わっていないことがわかると思います。

これはReactもまた関数が動いていることに変わりないためです。Layoutというコンポーネントが動作し、続いて中身のTitleが動作する、h1pが実行されて返される。
特に注目したい部分がitemListのフィルタリング部分です。関数がただ上から実行されているだけですので、特殊な関数を用意せずとも上から順番に実行されフィルタリングされた値が出力されます

しかし、このままだと逐一ソースのflagを変更しなければなりません。したがって最後にReactの機能を用いてフラグを更新する機能を追加してみましょう。

Step6. フラグの更新機能を追加する

flagのstateやflagのトグル関数、発火用のbuttonなどを追加しました。

const List = () => {
  //itemListをフィルタリングするかどうかのフラグ
  const [filterlingFlag, setFilterlingFlag] = useState(true);
  const toggleFilterlingFlag = () => {
    setFilterlingFlag((prevState) => !prevState);
  };

  const itemList = [
    { title: "タイトル1", text: "テキスト1", flag: true },
    { title: "タイトル2", text: "テキスト2", flag: false },
    { title: "タイトル3", text: "テキスト3", flag: true },
    { title: "タイトル4", text: "テキスト4", flag: false },
    { title: "タイトル5", text: "テキスト5", flag: true },
    { title: "タイトル6", text: "テキスト6", flag: false }
  ];

  //flagによってフィルタリングする
  const filteredItemList = itemList.filter((item) => {
    return item.flag === filterlingFlag;
  });

  return (
    <>
      <ul className="list">
        {filteredItemList.map((item) => (
          <ListItem {...item} />
        ))}
      </ul>
      <button onClick={toggleFilterlingFlag}>
        {filterlingFlag ? "true" : "false"}
      </button>
    </>
  );
};

これによりflagのtrue/falseが切り替えられるようになっています。

ここで、stateの役割が変更時の関数再実行(呼び出し元のコンポーネント再レンダリング)関数実行前後の値の保持の2つであることを抑えておきましょう。するとボタン押下時に行われていることは、「stateの更新・更新されたstateを使っての関数再実行」ということがわかると思います。

まとめ

Reactのメンタルモデルをざっくり掴むことを目的に、素のHTMLとJSを用いて関数によるページ作成を行ってみました。Reactを学ぶとstateやそれに伴う状態の扱いに混乱しがちですが、あくまで関数が実行されているという意識を持つと全体像が掴みやすくなると思います。
何かと新しい要素が多く混乱しがちなReactですが、その際にはこちらの記事を少しでも思い出していただけると嬉しいです。

さらに詳しく仕様を知りたい方は新しいReactドキュメントがおすすめです。英語なのでDeepLとなどと一緒に🤟

https://beta.reactjs.org/

GitHubで編集を提案

Discussion