【図解ハンズオン】たった60分でReactを使った音楽プレイヤーを作ろう！【TypeScript/Shadcn/TailwindCSS】

![react_music_player_handson.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/a21b87f1-44ae-c456-f0a8-d35f85069bb2.png)

# はじめに

こんにちは、Watanabe Jin([@Sicut_study](https://x.com/Sicut_study))です。
普段はReactを使った実践的なハンズオンを投稿しています。

![バズグルメ誕生 (11).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/f9bd83a6-ddab-c6da-71a2-4fbf347beac2.gif)

今回はApple Musicのようなかっこいい音楽プレイヤーを作成したいと思います。
実際にデプロイすればオンラインで音楽を聞くことができる実用的なものになっているので、ぜひハンズオンをして学んでみてください。

# 動画で解説

この記事よりももっと細かいところまで知りたい方は動画教材もあります。
もしReactやTypeScriptに不安がある方、やったことがない方は以下の動画をみてください！

<iframe width="560" height="315" src="https://www.youtube.com/embed/bQDRJuSbKkI?si=dhFH0fAbGHNr3IEf" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

# 対象者

- Reactを初めてやる
- HTMLの経験がある
- JavaScriptをもっと学びたい
- TypeScirptを学んでみたい

今回のハンズオンはReactの基本が学べるので、Reactを全くやったことがない人におすすめです。

# 1. Reactの環境構築

Reactとかっこいい画面を簡単に作れるコンポーネントライブラリShadcnが実行できる環境を用意します。
Node.jsが実行できる環境がお手元にない方は以下を参考にそれぞれのOSにあった方法でインストールしてください！

https://nodejs.jp/

https://www.sejuku.net/blog/72545

インストールができたことを以下の確認で確認してください

```
❯ node -v
v18.17.0
```

Reactのプロジェクトを構築します。
今回はViteを利用していきます。Viteは次世代のビルドツールで早くて無駄のない環境を提供してくれます。

https://ja.vitejs.dev/guide/

```
❯ npm create vite
Need to install the following packages:
  create-vite@5.5.2
Ok to proceed? (y) y
✔ Project name: … music-player
✔ Select a framework: › React
✔ Select a variant: › TypeScript

$ cd music-player/
$ npm i
$ npm run dev
```

[http://localhost:5173](http://localhost:5173)にアクセスして以下の画面が表示されればReact環境が無事できています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/cc74191c-f3a2-533b-93c4-97c5fb0a8d9b.png)


次にShadcnを利用するために、Shadcnの内部で利用しているTailwindCSSを導入していきます。

```
$ npm install -D tailwindcss@3.4.13 postcss autoprefixer
$ npx tailwindcss init -p
```

プロジェクトをVSCodeで開いて`tailwind.config.js`を以下に変えます

```js:tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/**/*.{js,jsx,ts,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
```

`src/index.css`を変更します

```css:src/index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

`src/App.tsx`を変更してスタイルがあたるかをチェックします

```tsx:src/App.tsx
function App() {
  return (
    <>
      <div>
        <button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
          Button
        </button>
      </div>
    </>
  );
}

export default App;
```

一度サーバーを落として`npm run dev`で起動したらボタンが表示されました

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/a844a950-e881-3dac-16cb-792dd2c4f430.png)

TailwindCSSが導入できたので、Shadcnを導入します。

https://ui.shadcn.com/docs/installation/vite

`tsconfig.json`を以下に修正します。

```json:tsconfig.json
{
  "files": [],
  "references": [
    { "path": "./tsconfig.app.json" },
    { "path": "./tsconfig.node.json" }
  ],
    "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}
```

`tsconfig.app.json`を修正します。

```json:tsconfig.app.json
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "isolatedModules": true,
    "moduleDetection": "force",
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "baseUrl": ".",
    "paths": {
      "@/*": [
        "./src/*"
      ]
    }
  },
  "include": ["src"]
}
```

次に以下を実行します。

```
$ npm i -D @types/node
```

そして`vite.config.ts`を修正します。

```ts:vite.config.ts
import path from "path"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
})
```

それでは準備ができたので実際にShadcnを初期化して入れていきます。

```
$ npx shadcn@latest init

Need to install the following packages:
shadcn@2.1.0
Ok to proceed? (y) y
✔ Preflight checks.
✔ Verifying framework. Found Vite.
✔ Validating Tailwind CSS.
✔ Validating import alias.
✔ Which style would you like to use? › New York
✔ Which color would you like to use as the base color? › Neutral
✔ Would you like to use CSS variables for theming? … no / yes
✔ Writing components.json.
✔ Checking registry.
✔ Updating tailwind.config.js
✔ Updating src/index.css
✔ Installing dependencies.
✔ Created 1 file:
  - src/lib/utils.ts

Success! Project initialization completed.
You may now add components.
```

試しにボタンコンポーネントをダウンロードしてきて、使ってみます。

```
$ npx shadcn@latest add button
```

```tsx:App.tsx
import { Button } from "./components/ui/button";

function App() {
  return (
    <>
      <div>
        <Button>Click me</Button>
      </div>
    </>
  );
}

export default App;
```

画面を確認してみます。`npm run dev`でサーバーを起動した状態で確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/98dff88c-bd65-b80f-c947-7b2028688922.png)

環境構築がすべて終わりました。

# 2. 素材の用意

音源(mp3)とジャケット画像を用意する必要があるので準備していきます。
今回はYoutubeでおなじみのフリー音源である「魔王魂」から人気曲5つを使いたいと思います。

以下の素材をダウンロードして、ディレクトリの`public`に格納してください

音源は、「ダウンロード」をクリックして「mp3」を押すとダウンロードができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/34ea7681-c0d9-9dec-bb93-62d46d7bb7f1.png)

ジャケットはリンクから右クリックで画像を保存してください。


### 1. シャイニングスター

音源

https://maou.audio/14_shining_star/

ジャケット

https://www.google.com/search?sca_esv=0227903b37920102&q=%E3%82%B7%E3%83%A3%E3%82%A4%E3%83%8B%E3%83%B3%E3%82%B0%E3%82%B9%E3%82%BF%E3%83%BC&udm=2&fbs=AEQNm0BqbPbAzSj6PhNr7nv9Ltx-oFh8tVsgXi1MyFbswNtTUOS5b68chsyOj2QEdx4EPnMBV4jACP8lwMVxvbhBfvbrx4pWZKg2gD6tes49BzuxRVX_Xb5RU_RhSHACKAXJoRia8pgzgh3JfE1xgNrBEvZQ-89ZAWzkljSDnCKAhXbDiR3funD9X3mV47_BRjqdcZKeXl4ftyeNqwE1ZOaR65FaF1Ddqo2CQGx5BAuqel5DswTWK1E&sa=X&ved=2ahUKEwj9ocT414-JAxW5glYBHVfyB8EQtKgLegQIERAB&biw=1854&bih=1047&dpr=1#vhid=548qZNMMywrU7M&vssid=mosaic

### 2. 12345

音源

https://maou.audio/19_12345/

ジャケット

https://www.google.com/search?sca_esv=0227903b37920102&q=12345+%E9%AD%94%E7%8E%8B%E9%AD%82&udm=2&fbs=AEQNm0BqbPbAzSj6PhNr7nv9Ltx-oFh8tVsgXi1MyFbswNtTUOS5b68chsyOj2QEdx4EPnMBV4jACP8lwMVxvbhBfvbrx4pWZKg2gD6tes49BzuxRVX_Xb5RU_RhSHACKAXJoRia8pgzgh3JfE1xgNrBEvZQ-89ZAWzkljSDnCKAhXbDiR3funD9X3mV47_BRjqdcZKeXl4ftyeNqwE1ZOaR65FaF1Ddqo2CQGx5BAuqel5DswTWK1E&sa=X&ved=2ahUKEwjb2YS02I-JAxVsh1YBHecfDAAQtKgLegQIGxAB&biw=1854&bih=1047&dpr=1#vhid=knn1i-zyqqcgpM&vssid=mosaic

### 3. Burning Heart

音源

https://maou.audio/08_burning_heart/

ジャケット

https://www.google.com/search?q=burning+heart+%E9%AD%94%E7%8E%8B%E9%AD%82&sca_esv=0227903b37920102&udm=2&biw=1854&bih=1047&ei=jwIOZ4eYOcbc2roP_by32AU&oq=bur%E9%AD%94%E7%8E%8B%E9%AD%82&gs_lp=Egxnd3Mtd2l6LXNlcnAiDGJ1cumtlOeOi-mtgioCCAAyBhAAGAcYHjIGEAAYBxgeMgYQABgHGB5I70FQ5AtYnzRwA3gAkAEAmAF5oAHsBaoBAzYuMrgBA8gBAPgBAfgBApgCB6ACgATCAgUQABiABMICBhAAGAgYHsICCBAAGAcYCBgemAMAkgcDNS4yoAf3CQ&sclient=gws-wiz-serp#imgrc=jN8A_EllgsFGhM&imgdii=Vu3th5z25w3BkM

### 4. ハルジオン

音源

https://maou.audio/05_halzion/

ジャケット

https://www.google.com/search?q=%E3%83%8F%E3%83%AB%E3%82%B8%E3%82%AA%E3%83%B3+%E9%AD%94%E7%8E%8B%E9%AD%82&sca_esv=0227903b37920102&udm=2&biw=1854&bih=1047&ei=DAMOZ6KREL3m2roP_9HpqQk&ved=0ahUKEwiix4fw2I-JAxU9s1YBHf9oOpUQ4dUDCBA&uact=5&oq=%E3%83%8F%E3%83%AB%E3%82%B8%E3%82%AA%E3%83%B3+%E9%AD%94%E7%8E%8B%E9%AD%82&gs_lp=Egxnd3Mtd2l6LXNlcnAiGeODj-ODq-OCuOOCquODsyDprZTnjovprYIyBRAAGIAEMgYQABgHGB4yBxAAGIAEGBgyBxAAGIAEGBgyBxAAGIAEGBgyBxAAGIAEGBgyBxAAGIAEGBhI0SNQ-g9Y1iBwAHgAkAEAmAGNAaAB0gOqAQM0LjG4AQPIAQD4AQGYAgGgAkyYAwCSBwExoAfUBA&sclient=gws-wiz-serp#vhid=LC9kK5HcTkSWnM&vssid=mosaic

### 5. Bipolar Disorder Outside ver.

音源

https://maou.audio/40_bipolar_disorder_outside/

ジャケット

https://www.google.com/search?q=Bipolar+Disorder+Outside+ver.+%E9%AD%94%E7%8E%8B%E9%AD%82&sca_esv=0227903b37920102&udm=2&biw=1854&bih=1047&ei=JQMOZ5nAEvip2roPuISm4AE&ved=0ahUKEwjZ5v_72I-JAxX4lFYBHTiCCRwQ4dUDCBA&uact=5&oq=Bipolar+Disorder+Outside+ver.+%E9%AD%94%E7%8E%8B%E9%AD%82&gs_lp=Egxnd3Mtd2l6LXNlcnAiJ0JpcG9sYXIgRGlzb3JkZXIgT3V0c2lkZSB2ZXIuIOmtlOeOi-mtgkiWAVAAWABwAHgAkAEAmAFZoAFZqgEBMbgBA8gBAPgBAvgBAZgCAKACAJgDAJIHAKAHMw&sclient=gws-wiz-serp#vhid=mQHY4OpLnes3QM&vssid=mosaic

名前は分かりづらいので、曲名にしましょう。
最終的にはこのようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/06011da2-07fd-118e-63dd-66e61e7572fd.png)

# 3. ジャケットを切り替える

まずはジャケットを切り替える実装をしていきましょう

```tsx:App.tsx
import { useState } from "react";

type Song = {
  id: number;
  title: string;
  artist: string;
  coverUrl: string;
  musicUrl: string;
};

const songs: Song[] = [
  {
    id: 1,
    title: "シャイニングスター",
    artist: "詩歩",
    coverUrl: "/shining_star.jpg",
    musicUrl: "/shining_star.mp3",
  },
  {
    id: 2,
    title: "Burning Heart",
    artist: "KEI",
    coverUrl: "/burning_heart.jpg",
    musicUrl: "/burning-heart.mp3",
  },
  {
    id: 3,
    title: "12345",
    artist: "Mary",
    coverUrl: "/12345.jpg",
    musicUrl: "/12345.mp3",
  },
  {
    id: 4,
    title: "ハルジオン",
    artist: "KEI",
    coverUrl: "/halzion.jpg",
    musicUrl: "/halzion.mp3",
  },
  {
    id: 5,
    title: "Bipolar Disorder Outside ver.",
    artist: "森田交一",
    coverUrl: "/outside.png",
    musicUrl: "/outside.mp3",
  },
]

function App() {
  const [currentSongIndex, setCurrentSongIndex] = useState(0);
  const currentSong = songs[currentSongIndex];

  const handlePrevious = () => {
    setCurrentSongIndex(
      (prevIndex) => (prevIndex - 1 + songs.length) % songs.length
    );
  };

  const handleNext = () => {
    setCurrentSongIndex((prevIndex) => (prevIndex + 1) % songs.length);
  };

  return (
    <div>
      <div>
        <img src={currentSong.coverUrl} alt="Cover" />
      </div>
      <div>
        <h2>{currentSong.title}</h2>
        <p>{currentSong.artist}</p>
      </div>
      <div>
        <button onClick={handlePrevious}>戻る</button>
        <button onClick={handleNext}>次へ</button>
      </div>
    </div>
  );
}

export default App;
```
　

「次へ」「戻る」でジャケットを切り替えることができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/caaa698f-23b1-cf0c-1fe7-d3b7ef82d9f6.png)

今回はReactの肝になる`useState`を利用して画像変更をしているので、ここから解説していきます。
例えば、ボタンを押したらタイトルを切り替えるようなアプリを作ったとします。

ここでボタンの`onClick`(クリックしたら実行する関数)でタイトルの文字列を変更したとします。
ボタンをクリックしたときにtitleの値は`新しいタイトル`に変わるのですが、画面は変わらないのです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/53dbf8f8-7e2d-ce73-515e-653f55b836fc.png)

ここで利用するのが`State`になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/2886af3f-b68e-59d7-7c7c-0d30e42fe38d.png)

`state`を利用することで値の変更をReactが検知してくれて、変更があったときに画面を再レンダリングしてくれます。これによりタイトルが変更されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/21251c69-e211-a243-2087-0827c0ac7df5.png)

再レンダリングはこのような仕組みになっています。
実際に画面に表示されているのがDOMといい、Reactではstateで管理された値が変更されたら、その変更差分を仮想DOMと言われるDOMのコピーに対して変更を反映します。そのあとで実際のDOMに変更を反映するため再レンダリングされて画面が切り替わります。これは`useState`を利用することで実現できることなのです。
　

 コードを解説していきます。

```tsx
  const [currentSongIndex, setCurrentSongIndex] = useState(0);
  const currentSong = songs[currentSongIndex];
```

今回は現在のジャケットのIDを`useState`で管理できるようにしています。
このステートを変えることでジャケット画像を変更して再レンダリングで画面を更新することができます。
`currentSong`を使ってジャケット、タイトル、アーティストを画面に表示しています。
「次へ」「戻る」を押すと、`currentSongIndex`のステートが更新されるため、再描画が走り画面も切り替わるようになっています。

ボタンにはそれぞれクリックしたときのアクションが`onClick`で設定されています。

```tsx
        <button onClick={handlePrevious}>戻る</button>
        <button onClick={handleNext}>次へ</button>
```

例えば、次へボタンを押したとすると以下の関数が実行されます。

```tsx
  const handleNext = () => {
    setCurrentSongIndex((prevIndex) => (prevIndex + 1) % songs.length);
    setIsPlaying(false);
  };
```

例えば、1つ目のジャケットから次に進もうしたときには、

prevIndex : 0 (配列のインデックスは0からスタート)
prevIndex + 1 : 1
songs.length : 5

つまり、1 % 5で5で割ったときの余りである1となります。
それが`currentSongIndex`にセットされることで、`currentSong`は`songs[0]`->`songs[1]`へと変わります。

曲を切り替えたら再生マークが表示されるように`IsPlaying`も`false`へと変えます。

# 4. 音楽を再生する

次は`currentSong`の音楽を再生できるようにしていきます。
ここでは`audioタグ`を利用することで再生をします。

```tsx:src/App.tsx
import { useState, useRef } from "react";

type Song = {
  title: string;
  artist: string;
  coverUrl: string;
  musicUrl: string;
};

const songs: Song[] = [
  {
    title: "シャイニングスター",
    artist: "詩歩",
    coverUrl: "public/shining_star.jpg",
    musicUrl: "shining_star.mp3",
  },
  {
    title: "Burning Heart",
    artist: "KEI",
    coverUrl: "public/burning_heart.jpg",
    musicUrl: "burning_heart.mp3",
  },
  {
    title: "12345",
    artist: "Mary",
    coverUrl: "public/12345.jpg",
    musicUrl: "12345.mp3",
  },
  {
    title: "ハルジオン",
    artist: "KEI",
    coverUrl: "public/halzion.jpg",
    musicUrl: "halzion.mp3",
  },
  {
    title: "Bipolar Disorder Outside ver.",
    artist: "森田交一",
    coverUrl: "public/outside.png",
    musicUrl: "outside.mp3",
  },
];

function App() {
  const [currentSongIndex, setCurrentSongIndex] = useState(0);
  const [isPlaying, setIsPlaying] = useState(false);
  const audioRef = useRef<HTMLAudioElement>(null);
  const currentSong = songs[currentSongIndex];

  const handlePrevious = () => {
    setCurrentSongIndex(
      (prevIndex) => (prevIndex - 1 + songs.length) % songs.length
    );
    setIsPlaying(false);
  };

  const handleNext = () => {
    setCurrentSongIndex((prevIndex) => (prevIndex + 1) % songs.length);
    setIsPlaying(false);
  };

  const togglePlayPause = () => {
    if (!audioRef.current) return;

    if (isPlaying) {
      audioRef.current.pause();
    } else {
      audioRef.current.play();
    }
    setIsPlaying(!isPlaying);
  };

  return (
    <div>
      <div>
        <img
          src={currentSong.coverUrl}
          alt="Cover"
          style={{ width: "300px", height: "300px" }}
        />
      </div>
      <div>
        <h2>{currentSong.title}</h2>
        <p>{currentSong.artist}</p>
      </div>
      <div>
        <button onClick={handlePrevious}>戻る</button>
        <button onClick={togglePlayPause}>
          {isPlaying ? "一時停止" : "再生"}
        </button>
        <button onClick={handleNext}>次へ</button>
      </div>
      <audio ref={audioRef} src={currentSong.musicUrl} onEnded={handleNext} />
    </div>
  );
}

export default App;
```

こんな感じの音楽プレイヤーができました。裏ではしっかり音楽がかかっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/bf60a5aa-0ee8-1640-416b-6b5a3b13529e.png)


React では、useRef を使って <audio> 要素を参照し、JavaScript での制御を行います。これにより、ボタンのクリックで音楽を再生・停止する機能を簡単に実装できます。

例えば、useRef を使って、以下のように音楽の再生と停止を行います。

```tsx
const audioRef = useRef(null);

const playAudio = () => {
  audioRef.current.play();
};

return (
  <audio ref={audioRef} src="song.mp3" />
  <button onClick={playAudio}>再生</button>
);
```

useRefを使うことで、仮想DOMではなく実際のDOMの要素を操作することが可能です。
通常、Reactは仮想DOM（virtual DOM）という仕組みを使って効率的にUIを更新していますが、特定のシナリオでは、実際のDOMに直接アクセスし操作する必要がある場合があります。今回の音楽再生は`useRef`を利用します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/24ab086d-38ce-f12c-98bc-e99580001813.png)

音楽などリアルタイム性が求められるものは再レンダリングで影響を受けてパフォーマンス低下になる恐れがあるため、直接DOMを操作することが一般的です。


```tsx
const [isPlaying, setIsPlaying] = useState(false);
```

新しく再生されているかどうかを判断するステートを追加しました。
ボタンをクリックすることで、trueならfalse、falseならtrueに切り替えます。

```tsx
  const togglePlayPause = () => {
    if (!audioRef.current) return;

    if (isPlaying) {
      audioRef.current.pause();
    } else {
      audioRef.current.play();
    }
    setIsPlaying(!isPlaying);
  };
```

`audio`タグの`onEmded`に関数を渡すことで曲が終了したら`handleNext`を実行することができます。これにより曲が終わったら自動で次の曲になります。

```tsx
<audio ref={audioRef} src={currentSong.musicUrl} onEnded={handleNext} />
```

# スタイルを当てていく

ここからはShadcnとTailwindCSSを使ってかっこいいスタイルに仕上げていきます。

まずは今回必要なコンポーネントをすべてインストールします。

```
$ npx shadcn add card
$ npx shadcn add slider
```

ボタンは先程入れているものがあるのでそちらを利用します。

```tsx:src/App.tsx
import { useState, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Pause, Play, SkipBack, SkipForward, Volume2 } from "lucide-react";
import { Slider } from "./components/ui/slider";

type Song = {
  title: string;
  artist: string;
  coverUrl: string;
  musicUrl: string;
};

const songs: Song[] = [
  {
    title: "シャイニングスター",
    artist: "詩歩",
    coverUrl: "public/shining_star.jpg",
    musicUrl: "shining_star.mp3",
  },
  {
    title: "Burning Heart",
    artist: "KEI",
    coverUrl: "public/burning_heart.jpg",
    musicUrl: "burning_heart.mp3",
  },
  {
    title: "12345",
    artist: "Mary",
    coverUrl: "public/12345.jpg",
    musicUrl: "12345.mp3",
  },
  {
    title: "ハルジオン",
    artist: "KEI",
    coverUrl: "public/halzion.jpg",
    musicUrl: "halzion.mp3",
  },
  {
    title: "Bipolar Disorder Outside ver.",
    artist: "森田交一",
    coverUrl: "public/outside.png",
    musicUrl: "outside.mp3",
  },
];

function App() {
  const [currentSongIndex, setCurrentSongIndex] = useState(0);
  const [isPlaying, setIsPlaying] = useState(false);
  const [volume, setVolume] = useState(50);
  const audioRef = useRef<HTMLAudioElement>(null);
  const currentSong = songs[currentSongIndex];

  const handlePrevious = () => {
    setCurrentSongIndex(
      (prevIndex) => (prevIndex - 1 + songs.length) % songs.length
    );
    setIsPlaying(false);
  };

  const handleNext = () => {
    setCurrentSongIndex((prevIndex) => (prevIndex + 1) % songs.length);
    setIsPlaying(false);
  };

  const togglePlayPause = () => {
    if (!audioRef.current) return;

    if (isPlaying) {
      audioRef.current.pause();
    } else {
      audioRef.current.play();
    }
    setIsPlaying(!isPlaying);
  };

  const handleVolumeChange = (value: number[]) => {
    setVolume(value[0]);
    if (audioRef.current) {
      audioRef.current.volume = value[0] / 100;
    }
  };

  return (
    <div className="flex items-center justify-center min-h-screen bg-gradient-to-br from-blue-900 to-gray-900">
      <Card className="w-full max-w-md bg-gradient-to-br from-gray-900 to-gray-800 text-white shadow-xl">
        <CardContent className="p-6">
          <div className="relative aspect-square mb-6 overflow-hidden rounded-lg shadow-2xl">
            <img
              src={currentSong.coverUrl}
              alt="Cover"
              className="w-full h-full object-cover transition-transform duration-500 ease-out hover:scale-110"
            />
          </div>
          <div className="text-center mb-6">
            <h2 className="text-2xl font-bold text-white mb-1">
              {currentSong.title}
            </h2>
            <p className="text-gray-400">{currentSong.artist}</p>
          </div>
          <div className="flex justify-between items-center mb-6">
            <Button
              variant="ghost"
              size="icon"
              onClick={handlePrevious}
              className="text-white hover:text-gray-300 transition-colors"
            >
              <SkipBack className="h-6 w-6" />
            </Button>
            <Button
              variant="ghost"
              size="icon"
              onClick={togglePlayPause}
              className="text-white hover:text-gray-300 transition-colors"
            >
              {isPlaying ? (
                <Pause className="h-8 w-8" />
              ) : (
                <Play className="h-8 w-8" />
              )}
            </Button>
            <Button
              variant="ghost"
              size="icon"
              onClick={handleNext}
              className="text-white hover:text-gray-300 transition-colors"
            >
              <SkipForward className="h-6 w-6" />
            </Button>
          </div>
          <div className="mt-6 flex items-center">
            <Volume2 className="h-4 w-4 text-gray-400 mr-2" />
            <Slider
              value={[volume]}
              max={100}
              step={1}
              className="w-full"
              onValueChange={handleVolumeChange}
            />
          </div>
          <audio
            ref={audioRef}
            src={currentSong.musicUrl}
            onEnded={handleNext}
          />
        </CardContent>
      </Card>
    </div>
  );
}

export default App;
```

このようなデザインになったら正しくスタイルが当てられています。

![バズグルメ誕生 (11).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/f9bd83a6-ddab-c6da-71a2-4fbf347beac2.gif)

ここで追加の機能として音量調整機能を実装したので解説します。
まずは音量を管理するステートを作りました。

```tsx
const [volume, setVolume] = useState(50) // 初期音量を50%に設定
```

volume状態で現在の音量値をパーセンテージ（0～100）で管理します。初期値は50%に設定されています。

音量の変更イベントは以下のようになっています。

```tsx
const handleVolumeChange = (value: number[]) => {
  setVolume(value[0]) // スライダーの値をvolume状態に反映
  if (audioRef.current) {
    audioRef.current.volume = value[0] / 100 // 0～1に変換しaudio要素のvolumeに設定
  }
}
```

SliderのonValueChangeイベントで呼び出され、スライダーが変更されるたびにこの関数が実行されます。

setVolume(value[0])でスライダーの値がvolume状態に反映されます。
audioRef.current.volume = value[0] / 100でスライダー値を0～1の範囲に変換し、audio要素のvolumeプロパティに設定されます。これにより、音量が即座に反映されます。

```tsx
<Slider
  value={[volume]}
  max={100}
  step={1}
  className="w-full"
  onValueChange={handleVolumeChange}
/>
```

onValueChange={handleVolumeChange}によりスライダーが変更されるたびに音量が更新されます。

# おわりに

今回はReactの基本的な機能を利用して音楽プレイヤーを作成しました。
Stateをしっかりと使いこなせれば簡単にアプリを作成することができます。

今回の内容は動画で更に詳しく解説していますので、合わせてご利用ください。

<iframe width="560" height="315" src="https://www.youtube.com/embed/bQDRJuSbKkI?si=dhFH0fAbGHNr3IEf" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

https://github.com/jinwatanabe?tab=repositories

　

他にも多くのハンズオンを投稿していますのでステップアップとしてぜひやってみてください。

https://qiita.com/Sicut_study/items/e3abe7624af8764258b9

https://qiita.com/Sicut_study/items/d3ef6d1347515c65a713


　
現役エンジニア専門のプログラミングコーチングを運営しています。
Reactを専門に自由に扱えるようになるので効率よく学びたい方はぜひカウンセリングでお待ちしております。

https://projisou.jp

ここまで読んでいただけた方はいいねとストックよろしくお願いします。
@Sicut_study をフォローいただけるととてもうれしく思います。

また明日の記事でお会いしましょう！

# JISOUのメンバー募集中！
プログラミングコーチングJISOUでは、新たなメンバーを募集しています。
実践的なカリキュラムで、あなたのエンジニアとしてのキャリアを最短で飛躍させましょう！
興味のある方は、ぜひホームページからお気軽にカウンセリングをお申し込みください！
▼▼▼

https://projisou.jp