無限スクロールでは、最初からすべての投稿をHTMLに書くのではなく、必要なタイミングでJavaScriptから投稿カードを追加します。
そのため、JavaScriptでHTML要素を作り、画面に表示するDOM操作の理解が重要です。
この記事では、SNS風フィードを例に、投稿データを作る方法、投稿カードを生成する方法、複数の投稿を効率よく追加する方法、クリックイベントを設定する方法を初心者向けに解説します。
JavaScriptで投稿を生成する考え方
通常のHTMLでは、投稿を1件ずつ直接書きます。
<article class="post">
<p>投稿本文</p>
</article>
しかし、無限スクロールでは投稿数が増え続けます。
そのため、HTMLにすべて書くのではなく、JavaScriptで必要な分だけ作ります。
流れは次の通りです。
- 投稿データを用意する
- 投稿カードを作る関数を用意する
- 作った投稿カードをフィードに追加する
- 必要になったら次の投稿を追加する
この仕組みを作ることで、APIから取得したデータを一覧に表示できるようになります。
投稿データを配列で用意する
まずは、投稿の材料になるデータを配列で用意します。
const names = ["Aoi", "Haruto", "Yuki", "Sora", "Ren"];
const handles = ["web_note", "design_lab", "code_days"];
const texts = [
"今日はJavaScriptで投稿カードを作っています。",
"無限スクロールの仕組みが少しずつわかってきました。",
"CSSとJavaScriptを組み合わせると、UIの幅が広がります。"
];
これは学習用のサンプルです。
本番では、この部分がAPIから取得したデータに置き換わります。
たとえば、次のようなJSONデータを受け取るイメージです。
{
"id": 1,
"name": "Aoi",
"handle": "web_note",
"text": "今日はJavaScriptで投稿カードを作っています。",
"likes": 24
}
データと表示処理を分けておくと、あとからAPI連携に変更しやすくなります。
ランダムにデータを取り出す関数
学習用のデモでは、配列からランダムに1つ取り出す関数を作っておくと便利です。
function pick(array) {
return array[Math.floor(Math.random() * array.length)];
}
この関数は、配列の中からランダムに1つの要素を返します。
たとえば、次のように使えます。
const name = pick(names);
const handle = pick(handles);
const text = pick(texts);
これで、投稿者名、ID、本文をランダムに組み合わせた投稿を作れます。
数字を見やすく整える関数
SNS風のUIでは、いいね数や閲覧数を短く表示することがあります。
たとえば、1200を 1.2k のように表示する形式です。
function formatNumber(number) {
if (number >= 1000) {
return (number / 1000).toFixed(1) + "k";
}
return String(number);
}
このような表示用の関数を作っておくと、UI側のコードが読みやすくなります。
投稿カードを作る関数
次に、投稿カードを作る関数を用意します。
let postId = 0;
function createPost() {
const name = pick(names);
const handle = pick(handles) + Math.floor(Math.random() * 100);
const text = pick(texts);
const likes = Math.floor(Math.random() * 500);
const article = document.createElement("article");
article.className = "post";
article.dataset.id = ++postId;
article.innerHTML = `
<div class="avatar">${name.charAt(0)}</div>
<div class="post-body">
<div class="post-meta">
<span class="name">${name}</span>
<span class="handle">@${handle}</span>
<span class="time">たった今</span>
</div>
<p class="post-text">${text}</p>
<div class="actions">
<button type="button" class="action comment">返信 <span>0</span></button>
<button type="button" class="action repost">リポスト <span>0</span></button>
<button type="button" class="action like">いいね <span>${formatNumber(likes)}</span></button>
</div>
</div>
`;
return article;
}
document.createElement("article") で、投稿カード用の article 要素を作っています。
className でCSS用のクラスを設定し、dataset.id で投稿IDを持たせています。
dataset.id はHTML上では data-id として扱われます。
投稿の識別、クリック計測、詳細ページへの遷移などで使えます。
innerHTMLを使うときの注意点
上の例では、innerHTML を使って投稿カードの中身を作っています。
innerHTML を使うと、HTMLの形をそのまま書けるので見通しが良くなります。
ただし、本番でユーザーが入力した文章や外部APIから取得した文字列をそのまま innerHTML に入れるのは危険です。
悪意のあるHTMLやスクリプトが混ざると、XSSというセキュリティ問題につながる可能性があります。
学習用の固定データなら問題ありませんが、本番では次のような対策を考えます。
- 信頼できない文字列は
textContentで入れる - HTMLを許可する場合はサニタイズする
- ユーザー投稿をそのまま
innerHTMLに入れない
便利な書き方ほど、セキュリティには注意が必要です。
投稿を画面に追加する
作成した投稿カードは、フィードエリアに追加します。
<main id="feed" class="feed"></main>
const feed = document.querySelector("#feed");
const post = createPost();
feed.appendChild(post);
appendChild() を使うと、指定した要素の最後に新しい要素を追加できます。
これで、JavaScriptから投稿カードを画面に表示できます。
複数の投稿をまとめて追加する
無限スクロールでは、1件ずつではなく、10件や20件の投稿をまとめて追加することが多いです。
そのときに便利なのが DocumentFragment です。
function loadMore(count = 10) {
const fragment = document.createDocumentFragment();
for (let i = 0; i < count; i++) {
fragment.appendChild(createPost());
}
feed.appendChild(fragment);
}
DocumentFragment は、画面には直接表示されない一時的な入れ物です。
投稿を一度この中にまとめてから、最後に feed へ追加します。
これにより、DOM更新の回数を抑えやすくなります。
小さなデモでは体感差は少ないですが、無限スクロールのように何度も要素を追加するUIでは、こうした考え方が大切です。
二重読み込みを防ぐ
無限スクロールでは、読み込み処理が連続して何度も呼ばれることがあります。
それを防ぐために、読み込み中かどうかを管理するフラグを用意します。
let loading = false;
function loadMore(count = 10) {
if (loading) return;
loading = true;
setTimeout(() => {
const fragment = document.createDocumentFragment();
for (let i = 0; i < count; i++) {
fragment.appendChild(createPost());
}
feed.appendChild(fragment);
loading = false;
}, 600);
}
loading が true の間は、新しい読み込みを実行しません。
これにより、同じデータが重複表示されたり、表示順が崩れたりするのを防げます。
本番では、API通信が始まったら loading = true、通信が終わったら loading = false にします。
クリックイベントを設定する
投稿カード内のいいねボタンをクリックしたときに、いいね数を増やす処理を追加します。
function setupActions(article) {
const likeButton = article.querySelector(".like");
likeButton.addEventListener("click", event => {
event.stopPropagation();
likeButton.classList.toggle("liked");
const span = likeButton.querySelector("span");
const current = Number(span.textContent.replace("k", "")) || 0;
if (likeButton.classList.contains("liked")) {
span.textContent = current + 1;
} else {
span.textContent = Math.max(0, current - 1);
}
});
}
event.stopPropagation() は、クリックイベントの伝播を止める指定です。
たとえば、投稿カード全体にもクリックイベントがある場合、いいねボタンを押しただけなのに投稿詳細へ移動してしまうことがあります。
そのような誤動作を防ぐために使います。
投稿生成時にイベントも設定する
createPost() の最後で setupActions(article) を呼び出すと、投稿を作るたびにイベントを設定できます。
function createPost() {
const name = pick(names);
const handle = pick(handles) + Math.floor(Math.random() * 100);
const text = pick(texts);
const likes = Math.floor(Math.random() * 500);
const article = document.createElement("article");
article.className = "post";
article.dataset.id = ++postId;
article.innerHTML = `
<div class="avatar">${name.charAt(0)}</div>
<div class="post-body">
<div class="post-meta">
<span class="name">${name}</span>
<span class="handle">@${handle}</span>
<span class="time">たった今</span>
</div>
<p class="post-text">${text}</p>
<div class="actions">
<button type="button" class="action comment">返信 <span>0</span></button>
<button type="button" class="action repost">リポスト <span>0</span></button>
<button type="button" class="action like">いいね <span>${formatNumber(likes)}</span></button>
</div>
</div>
`;
setupActions(article);
return article;
}
これで、新しく追加された投稿でも、いいねボタンが反応するようになります。
初期表示を作る
ページを開いた直後にも、最初の投稿を表示する必要があります。
loadMore(15);
最初に15件表示し、その後、スクロールに応じて10件ずつ追加するような構成にできます。
初期表示件数が少なすぎると、ページを開いた瞬間にローダーが見えてしまい、連続で読み込みが発生することがあります。
画面の高さや投稿カードのサイズに合わせて調整しましょう。
まとめ
JavaScriptで投稿カードを生成する流れは、無限スクロールの基本です。
ポイントは次の通りです。
- データと表示処理を分ける
createElement()で投稿要素を作るinnerHTMLを使う場合はセキュリティに注意する- 複数投稿は
DocumentFragmentでまとめて追加する loadingフラグで二重読み込みを防ぐ- クリックイベントは新しく作った要素にも設定する
この流れを理解すると、SNS風フィードだけでなく、記事一覧、商品一覧、ニュース一覧などにも応用できます。
次は、ページ下部に近づいたことを検知する IntersectionObserver を組み合わせることで、自動読み込みの仕組みを完成させます。
