テンプレートリテラルで掴むReactの動き方
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
が動作する、h1
やp
が実行されて返される。
特に注目したい部分が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となどと一緒に🤟
Discussion