はじめに
こんにちは!ブログアプリにいくつかの新機能を追加しました。この記事では、実装した3つの機能について、特にこだわった点や実装方法、そして学んだことを共有したいと思います。
以下の機能をブログアプリに追加しました。
- Markdownに対応したコメント機能
- スライドイン効果を持つToast通知
- 自動生成される記事目次
それぞれの機能の実装にこだわった点があるので、詳細に説明します。
コメント機能
実装のポイント
- Markdown形式でコメントを記載できるようにしました
- コメント投稿時にMarkdownのプレビューを表示する機能を追加し、ユーザーが投稿前に確認できるようにしました
- ReactのuseOptimisticフックを使用して楽観的更新(Optimistic UI)を実装しました
技術的な詳細
コメント機能では、ユーザー体験を向上させるために楽観的更新パターンを採用しています。ユーザーがコメントを投稿すると、APIレスポンスを待たずに即座にUIに反映され、バックグラウンドでサーバーとの同期が行われます。
// useOptimisticを使用した楽観的更新の例
const [optimisticComments, addOptimisticComment] = useOptimistic(
comments,
(state, newComment) => [...state, newComment]
);
const handleSubmit = async (formData) => {
// 楽観的にUIを更新
const newComment = {
id: `temp-${Date.now()}`,
content: formData.content,
author: currentUser,
createdAt: new Date().toISOString(),
isPending: true
};
addOptimisticComment(newComment);
// 実際のAPI呼び出し
try {
await postComment(formData);
// 成功した場合の処理
} catch (error) {
// エラー処理
showToast('コメントの投稿に失敗しました', 'error');
}
};
Toast通知の表示
実装のポイント
- 画面の右側からスライドインし、複数通知がある場合は下にスタックされるアニメーション効果を実装
- 一定時間経過後に自動的に消える機能を追加
- ReactのCompositionパターンを活用し、Toast用のContext/Provider(ClientComponent)の配下にServerComponentを配置する形にしました
技術的な詳細
Toastコンポーネントは、アプリケーション全体で一貫した通知UIを提供するために、Contextを使用して実装しました。これにより、どのコンポーネントからでも簡単にToast通知を表示できるようになりました。
"use client";
import clsx from "clsx";
import styles from "./index.module.scss";
import {
createContext,
CSSProperties,
ReactNode,
useContext,
useState,
} from "react";
import { Toast } from "../Toast";
type ToastContextType = {
queueToast: (title: string, detail: string) => Promise<void>;
};
type State = "visiable" | "hidden";
type ToastContent = {
id: string;
title: string;
detail: string;
state: State;
};
const ToastContext = createContext<ToastContextType>({
queueToast: async () => {},
});
// Contextを呼び出したいClientコンポーネントからuseToastContextを呼び出す
export const useToastContext = () => useContext(ToastContext);
export const ToastProvider = (props: { children: ReactNode }) => {
...
return (
<ToastContext.Provider
value={{
queueToast: setTimedToast,
}}
>
<div className={styles.toastProvider}>
...
</div>
</ToastContext.Provider>
);
};
詳細な実装はGitHubを参照ください。
目次の表示
実装のポイント
- 記事の見出し(h2, h3, h4など)を自動で解析して目次を生成
- 目次の項目をクリック/タップすると、対応する見出しまでスムーズにスクロールする機能を追加
技術的な詳細
目次の自動生成は、記事のマークダウンコンテンツをパースし、見出し要素を抽出することで実現しています。また、ページ内ナビゲーションにはスムーズスクロール効果を適用しています。
目次は描画されたHTMLからCSSセレクタで吸い上げて、Context/Providerでコンポーネント間でデータを連携する形にしています。
export const TableOfContentContextProvider = (props: PropsWithChildren) => {
const { children } = props;
const [headings, setHeadings] = useState<HeadingMap | null>(null);
const watchRef = useRef<HTMLDivElement>(null);
const loadHeadings = () => {
if (!watchRef.current) {
return;
}
const article = watchRef.current.querySelector("#article");
const elements = article?.querySelectorAll("h1,h2,h3");
if (!elements) {
return;
}
...
setHeadings(headingMap);
return () => {
setHeadings(null);
};
};
const cleanupHeadings = () => {
setHeadings(null);
document.children[0].scrollTo(0, 0);
};
const jumpToHeading = (key: string) => {
const heading = headings?.get(key);
if (heading) {
const h = document.querySelectorAll(heading.type);
h.forEach((e) => {
if (e.textContent === heading.content) {
e.scrollIntoView();
return;
}
});
}
};
return (
<TableOfContentContext.Provider
value={{
headingMap: headings,
loadHeadings: loadHeadings,
cleanupHeadings: cleanupHeadings,
jumpToHeading: jumpToHeading,
watchRef: watchRef,
}}
>
<div>{children}</div>
</TableOfContentContext.Provider>
);
};
詳細な実装はGitHubを参照ください。
今後の改善点
現在、スクロール位置に応じて目次のアクティブな項目をハイライトする機能はまだ実装できていません。Intersection Observer APIを使用して、表示中の見出しに合わせて目次のUI状態を更新する機能を追加する予定です。
まとめ
今回実装した3つの機能は、ユーザー体験の向上を主な目的としています。特にReactの最新機能であるuseOptimistic
を活用したコメント機能は、レスポンスの待ち時間を感じさせないUIを実現できました。
今後も引き続き、パフォーマンスとユーザー体験を両立させる実装を追求していきたいと思います。