展開した時にAPIコールが必要なドロップダウンメニューをServer Actionsを利用して作成する
📌 はじめに
こんにちは!@Ryo54388667です!☺️
普段は都内でフロントエンドエンジニアとして業務をしてます!
主にTypeScriptやNext.jsといった技術を触っています。
今回は、展開する時に、APIコールが必要なドロップダウンメニューをServer Actionsを利用して作成してみたので紹介します。少しニッチなユースケースかもしれませんが参考になれば幸いです。また、「こうした方が良いよ!」というアドバイスも大歓迎です!🙆♂️
📌 UIの仕様と課題
- クリックベースのドロップダウンメニュー。 ユーザーが何かをクリックした時にのみ、ドロップダウンメニューが表示されること。
- APIを利用した動的なメニュー表示。 展開時にAPIコールし、常に最新のものが表示されること。
このような仕様として考えます。
ともすると、Client Components ばかりになりそうだなーと思いました😅なぜなら、クリックイベントはClient Componentsで行う必要がありますし、ドロップダウンの箇所はopen OR close のstate管理が必要ですし、メニューに関してはRefを利用しそうですし。。。
かといって、展開時にAPIコールが必要なので、Client Componentsではなく、できればServer Componentsを利用したいなーと。。
こちらのページにあるような使い分けに準拠したいところです。
これらのことを踏まえて、このUIの実装方法を考えてみました!
📌 実装例
Server Actionsを利用して作成しました!
クリックイベントの関わるコンポーネント
// ui/SideNav.tsx
"use client";
import { useState } from "react";
import Collapse from "./Collapse";
import CollapseList from "./CollapseList";
type SelectedButton = "Button01" | "Button02" | "Button03";
export default function SideNav() {
const [selectedButton, setSelectedButton] = useState<SelectedButton | null>(null);
return (
<>
<button
type="button"
style={{ borderBottom: selectedButton === "Button01" ? "1px solid gray" : "1px solid black", fontSize: "50px" }}
onClick={() => {
setSelectedButton(selectedButton === "Button01" ? null : "Button01");
}}
>
Button 01
</button>
<Collapse isOpen={selectedButton === "Button01"}>
<CollapseList isOpen={selectedButton === "Button01"} postId={1} />
</Collapse>
//...(繰り返し)...
</>
);
}
Refの関わるコンポーネント
// ui/Collapse.tsx
"use client";
import { ReactNode, useEffect, useRef, useState } from "react";
type PropsType = {
isOpen: boolean;
children: ReactNode;
};
export default function Collapse({ isOpen, children }: PropsType) {
const [maxHeight, setMaxHeight] = useState('0px');
const divRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (divRef.current) {
setMaxHeight(isOpen ? `${divRef.current.scrollHeight}px` : '0px');
}
}, [isOpen]);
return (
<div
style={{ maxHeight, overflow: "hidden", transition: 'max-height 0.3s ease-in-out' }}
ref={divRef}
>
{children}
</div>
);
}
メニューが開いた時に表示されるコンポーネント
// ui/CollapseList.tsx
"use client";
import { dropdownFetchAction } from "@/lib/action";
import { Post } from "@/types";
import { useEffect, useState } from "react";
type PropsType = {
isOpen: boolean;
postId: number;
};
export default function CollapseList({ isOpen, postId = 1 }: PropsType) {
const [posts, setPosts] = useState<{ data: any } | null>(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const fetchPost = async () => {
setIsLoading(true);
if (isOpen) {
// ✅ こちらがServer Actionsの関数
const data = await dropdownFetchAction(postId);
setPosts(data);
}
setIsLoading(false);
};
fetchPost();
}, [isOpen, postId]);
if (isLoading) {
return <div style={{ backgroundColor: "gray", color: "white" }}>ローディング中</div>;
}
return (
posts &&
posts.data.map((post: Post, index: any) => (
<li key={index} style={{ backgroundColor: "lightgreen", listStyleType: "none" }}>
リンク : {post.title}
</li>
))
);
}
データフェッチを行うActions
// lib/actions.ts
"use server"
export const dropdownFetchAction = async (id : number) => {
console.log("dropdownFetchAction");
// ローディングを明示するためにスリープ関数を設置している。
await new Promise(resolve => setTimeout(resolve, 2000))
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
const data = await res.json();
const random = Math.floor(Math.random() * 10) + 1
return {
data: Array.from({ length: random }, () => data),
}
}
こんな感じで標題のUIを作成しました。
少しニッチかもしれませんが、参考になれば幸いです!
「このUIをServer Componentsで作成するならコレ!」というようなベストプラクティスを知りたいところです。
リポジトリはこちらです!
最後まで読んでいただきありがとうございます!
気ままにつぶやいているので、気軽にフォローをお願いします!🥺
Discussion