0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[React] APIで取得したデータをタブ切り替え、リアルタイム検索してみる

Last updated at Posted at 2025-02-02

はじめに

アウトプット実践編として、APIで取得、タブ切り替え、リアルタイム検索をやってみたら色々勉強になったので記事にまとめようと思う。

環境

対応OS

  • Mac OS

対応バージョン

  • React 18.3.1
  • typescript 5.6.2

対応エディタ

  • VSCode

概要

  • ユーザー一覧と投稿一覧をタブで出し分け
  • ユーザー一覧はid、名前、メールアドレスを表示、投稿一覧はid、タイトル、内容を表示
  • それぞれに検索バーを設けて、ユーザー一覧の場合は名前を、投稿一覧の場合は内容をリアルタイム検索できるようにする
  • データ取得は以下URLから取得
  • CSSはtailwindcss v4

作業手順

  • APIでbaseURLを設定
  • APIで投稿一覧を取得
    • その中からID タイトル 内容を表示
  • APIでユーザー一覧を取得
    • その中からID 名前 メールアドレスを取得
  • リアルタイム検索機能を実装
  • ボタンで、投稿一覧とユーザー一覧を切り替える

1. 環境構築

terminal
 npm create vite@latest

プロジェクト名はカレントディレクトリ、使用するフレームワークはreact、TypeScriptを使用。
✔ Project name: … .
✔ Select a framework: › React
✔ Select a variant: › TypeScript + SWC

tailwindcss(v4)導入

terminal
 npm install tailwindcss @tailwindcss/vite

vite.config.tsに追記

vite.config.ts
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
  plugins: [
    tailwindcss(),
  ],
})

src/index.cssを書き換え

src/index.css
@import "tailwindcss";
terminal
 npm run dev

2. APIでbaseURLを設定

まず、JavaScriptで使えるHTTPクライアントライブラリaxiosをダウンロード。

terminal
 npm install axios

https://jsonplaceholder.typicode.com/の部分は共通しているのでコードを簡略化&再利用しやすくするためbaseURLを設定。

src/utils/api.ts
// axiosライブラリのインポートとAxiosインスタンスの作成
import axios from 'axios';

export default axios.create({
  baseURL: 'https://jsonplaceholder.typicode.com'
})

3. APIで各一覧を取得

tableのデザインはtailwindcssのコンポーネントとセットになっているFlowBiteを使用してスタイリング。
https://flowbite.com/docs/components/tables/

投稿一覧

src/pages/PostList.tsx
import { useEffect, useState } from 'react';
import api from '../utils/api';

type Post = {
  id: number;
  title: string;
  body: string;
};

export const PostList = () => {
  const [posts, setPosts] = useState<Post[]>([]);

  useEffect(() => {
    api
      .get('/posts')
      .then((response) => setPosts(response.data))
      .catch((error) => console.error('Error fetching posts:', error));
  }, []);

  return (
    <div className="relative overflow-x-auto">
      <table className="w-full text-sm text-left rtl:text-right text-gray-500">
        <thead className="text-xs text-gray-700 uppercase bg-gray-200">
          <tr>
            <th scope="col" className="px-6 py-3">
              id
            </th>
            <th scope="col" className="px-6 py-3">
              タイトル
            </th>
            <th scope="col" className="px-6 py-3">
              内容
            </th>
          </tr>
        </thead>
        <tbody>
          {posts.map((post) => (
            <tr
              key={post.id}
              className="bg-white border-b"
            >
              <td className="px-6 py-4">{post.id}</td>
              <td className="px-6 py-4">{post.title}</td>
              <td className="px-6 py-4">{post.body}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
};

ユーザー一覧

src/pages/UserList.tsx
import { useEffect, useState } from 'react';
import api from '../utils/api';

type User = {
  id: number;
  name: string;
  email: string;
}

export const UserList = () => {
  const [users, setUsers] = useState<User[]>([]);

  useEffect(() => {
    api.get('/users')
    .then(response => setUsers(response.data))
    .catch(error => console.error('Error fetching data:', error))
 }, []);

  return (
    <div className="relative overflow-x-auto">
      <table className="w-full text-sm text-left rtl:text-right text-gray-500">
        <thead className="text-xs text-gray-700 uppercase bg-gray-200">
          <tr>
            <th scope="col" className="px-6 py-3">
              id
            </th>
            <th scope="col" className="px-6 py-3">
              名前
            </th>
            <th scope="col" className="px-6 py-3">
              メールアドレス
            </th>
          </tr>
        </thead>
        <tbody>
          {users.map((user) => (
            <tr
              key={user.id}
              className="bg-white border-b"
            >
              <td className="px-6 py-4">{user.id}</td>
              <td className="px-6 py-4">{user.name}</td>
              <td className="px-6 py-4">{user.email}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
};

4. リアルタイム検索機能を実装

  • App.tsx
    useState を使って keyword(検索キーワード)の状態を管理。
src/App.tsx
import { useState } from 'react';
import './App.css'
import { SearchBox } from './components/SearchBox';
import { PostList } from './pages/PostList';
import { UserList } from './pages/UserList';

function App() {
  const [keyword, setKeyword] = useState<string>('');

  return (
    <>
      <SearchBox keyword={keyword} setKeyword={setKeyword} />
      <PostList keyword={keyword}  />
      <UserList keyword={keyword} />
    </>
  )
}

export default App
  • SearchBox.tsx
    keyword(現在の検索文字列)とsetKeyword(検索文字列を更新する関数)を受け取る。入力値が変わると、setKeywordで更新される。
src/components/SearchBox.tsx
import { FC } from 'react';

type SearchBoxProps = {
  keyword: string;
  setKeyword: React.Dispatch<React.SetStateAction<string>>;
};

export const SearchBox: FC<SearchBoxProps> = ({ keyword, setKeyword }) => {
  return (
    <div className="pb-4 bg-white max-w-md mx-auto">
      <label htmlFor="table-search" className="sr-only">Search</label>
      <div className="relative mt-1">
        <div className="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
          <svg className="w-4 h-4 text-gray-500 dark:text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
            <path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"/>
          </svg>
        </div>
        <input
          type="text"
          id="table-search"
          className="block py-2 ps-10 text-sm text-gray-900 border border-gray-300 rounded-lg w-full bg-gray-50 focus:ring-blue-500 focus:border-blue-500"
          placeholder="検索キーワードを入力"
          value={keyword}
          onChange={(e) => setKeyword(e.target.value)}
        />
      </div>
    </div>
  );
};
  • PostList.tsx
    useEffectを使ってAPIから投稿データを取得し、postsに保存。
    keywordが変更されたときにfilteredPostsを更新し、検索結果のみ表示。
    .toLowerCase()を使い、大文字・小文字の違いを無視して検索できるようする。
    bodyに対して検索を行う。
src/pages/PostList.tsx
import { useEffect, useState, FC, useMemo } from 'react';
import api from '../utils/api';

type Post = {
  id: number;
  title: string;
  body: string;
};

type PostListProps = {
  keyword: string;
};

export const PostList: FC<PostListProps> = ({ keyword }) => {
  const [posts, setPosts] = useState<Post[]>([]);

  useEffect(() => {
    api.get('/posts')
       .then(response => setPosts(response.data))
       .catch(error => console.error('Error fetching posts:', error));
  }, []);

  const filteredPosts = useMemo(() => {
    return posts.filter(post =>
      post.body.toLowerCase().includes(keyword.toLowerCase())
    );
  }, [keyword, posts]);

  return (
    <div className="relative overflow-x-auto">
      <table className="w-full text-sm text-left rtl:text-right text-gray-500">
        <thead className="text-xs text-gray-700 uppercase bg-gray-50">
          <tr>
            <th className="px-6 py-3">ID</th>
            <th className="px-6 py-3">タイトル</th>
            <th className="px-6 py-3">内容</th>
          </tr>
        </thead>
        <tbody>
          {filteredPosts.map((post) => (
            <tr key={post.id} className="bg-white border-b">
              <td className="px-6 py-4">{post.id}</td>
              <td className="px-6 py-4">{post.title}</td>
              <td className="px-6 py-4">{post.body}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
};
  • UserList.tsx
    PostList.tsxとほぼ同じ構造で、/usersからユーザー情報を取得。
    nameに対して検索を行う。
src/pages/UserList.tsx
import { useEffect, useState, FC, useMemo } from 'react';
import api from '../utils/api';

type User = {
  id: number;
  name: string;
  email: string;
};

type UserListProps = {
  keyword: string;
};

export const UserList: FC<UserListProps> = ({ keyword }) => {
  const [users, setUsers] = useState<User[]>([]);

  useEffect(() => {
    api.get('/users')
       .then(response => setUsers(response.data))
       .catch(error => console.error('Error fetching users:', error));
  }, []);

  const filteredUsers = useMemo(() => {
    return users.filter(user =>
      user.name.toLowerCase().includes(keyword.toLowerCase())
    );
  }, [keyword, users]);

  return (
    <div className="relative overflow-x-auto">
      <table className="w-full text-sm text-left rtl:text-right text-gray-500">
        <thead className="text-xs text-gray-700 uppercase bg-gray-50">
          <tr>
            <th className="px-6 py-3">ID</th>
            <th className="px-6 py-3">名前</th>
            <th className="px-6 py-3">メールアドレス</th>
          </tr>
        </thead>
        <tbody>
          {filteredUsers.map((user) => (
            <tr key={user.id} className="bg-white border-b">
              <td className="px-6 py-4">{user.id}</td>
              <td className="px-6 py-4">{user.name}</td>
              <td className="px-6 py-4">{user.email}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
};

5. ボタンで、投稿一覧とユーザー一覧を切り替える

  • TabButton.tsx
    isActiveに応じて、ボタンのデザインを変更。
src/components/TabButton.tsx
import React from 'react'

type TabButtonProps = {
  label: string;
  isActive: boolean;
  onClick: () => void;
};

export const TabButton: React.FC<TabButtonProps> = ({ label, isActive, onClick }) => {
  return (
    <button
      onClick={onClick}
      className={`inline-block border-solid border-1 border-gray-600 p-4 rounded ${
        isActive ? 'text-white bg-gray-600' : 'hover:text-gray-600 hover:bg-gray-50'
      }`}
    >
      {label}
    </button>
  )
}
  • App.tsx
    selectedTab(現在選択中のタブ)を userList(ユーザー一覧)か postList(投稿一覧)で管理。
    selectedTab の値に応じて、表示するコンポーネントを切り替え。
src/App.tsx
import { useState } from 'react';
import './App.css';
import { SearchBox } from './components/SearchBox';
import { PostList } from './pages/PostList';
import { UserList } from './pages/UserList';
import { TabButton } from './components/TabButton';

function App() {
  const [keyword, setKeyword] = useState<string>('');
  const [selectedTab, setSelectedTab] = useState<'userList' | 'postList'>('userList');

  return (
    <div className="container mx-auto p-4">
      <ul className="flex flex-wrap text-sm font-medium text-center text-gray-500 justify-center mb-4">
        <li className="me-2">
          <TabButton
            label="ユーザー一覧"
            isActive={selectedTab === 'userList'}
            onClick={() => setSelectedTab('userList')}
          />
        </li>
        <li className="me-2">
          <TabButton
            label="投稿一覧"
            isActive={selectedTab === 'postList'}
            onClick={() => setSelectedTab('postList')}
          />
        </li>
      </ul>
      <SearchBox keyword={keyword} setKeyword={setKeyword} />
      {selectedTab === 'userList' ? <PostList keyword={keyword} /> : <UserList keyword={keyword} />}
    </div>
  );
}

export default App;

スクリーンショット 2025-02-02 0.33.52.png

終わりに

インプットが済んであらかたできる気でいたが難しかった。
もっとアウトプットを増やして知識を身につけていきたい。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?