🍡

展開した時にAPIコールが必要なドロップダウンメニューをServer Actionsを利用して作成する

2024/01/05に公開

📌 はじめに

こんにちは!@Ryo54388667です!☺️

普段は都内でフロントエンドエンジニアとして業務をしてます!
主にTypeScriptやNext.jsといった技術を触っています。

今回は、展開する時に、APIコールが必要なドロップダウンメニューをServer Actionsを利用して作成してみたので紹介します。少しニッチなユースケースかもしれませんが参考になれば幸いです。また、「こうした方が良いよ!」というアドバイスも大歓迎です!🙆‍♂️

📌 UIの仕様と課題

  1. クリックベースのドロップダウンメニュー。 ユーザーが何かをクリックした時にのみ、ドロップダウンメニューが表示されること。
  2. APIを利用した動的なメニュー表示。 展開時にAPIコールし、常に最新のものが表示されること。

このような仕様として考えます。
ともすると、Client Components ばかりになりそうだなーと思いました😅なぜなら、クリックイベントはClient Componentsで行う必要がありますし、ドロップダウンの箇所はopen OR close のstate管理が必要ですし、メニューに関してはRefを利用しそうですし。。。

かといって、展開時にAPIコールが必要なので、Client Componentsではなく、できればServer Componentsを利用したいなーと。。
https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns#when-to-use-server-and-client-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で作成するならコレ!」というようなベストプラクティスを知りたいところです。

リポジトリはこちらです!

最後まで読んでいただきありがとうございます!
気ままにつぶやいているので、気軽にフォローをお願いします!🥺
https://twitter.com/Ryo54388667/status/1741284169370718631

Discussion