AstroでハンバーガーボタンをJavaScript/Reactで実装してみた

## はじめに
新規プロダクトのサービスサイトをAstroというフレームワークを使って実装することになりました。

Astroを使うにあたり、公式のチュートリアルで一通り学びました。
そのチュートリアルで、ハンバーガーボタンを実装する章があるのですが、JavaScriptで実装しています。

今回、サービスサイト実装にあたり、Reactでハンバーガーボタンを実装しました。ほとんどチュートリアルの内容と一緒なのですが、実装手順や違いなどを紹介したいと思います。

## Astroチュートリアルについて
公式が提供しているチュートリアルです。簡単なブログサイトを作ります。
やや翻訳が怪しいところはありますが日本語で読めますし、ボリュームもそこまで多くないので、2日あれば十分終わると思います。
※5章から急に難しくなるのでびっくりしないでください。

## 要件
以下のようなハンバーガーボタンをヘッダーに設置します。
ボタンをクリックすると、メニューが開きます。
画面の横幅が、636px以上の時は通常のヘッダーを、636px未満の時はハンバーガーボタンを表示します。

![スクリーンショット 2023-12-24 14.12.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3537832/92237295-6f68-5611-b610-28779f9bcfe8.png)

## JavaScriptで実装した場合（Astro公式チュートリアル）

まずはAstro公式チュートリアルの通りに、JavaScriptで実装します。
チュートリアルの、「3章Astro UIコンポーネントの作成と設計-4.初めてのスクリプトをブラウザに送信する」をやります。

途中の実装はすっ飛ばします。詳細はチュートリアルをご覧ください。

https://docs.astro.build/ja/tutorial/3-components/4/

### 1. ハンバーガーコンポーネントを作成する
3つ並んでいる`<span></span>`がハンバーガーボタンの三本線を表しています。
これにCSSでstyleを当てるとWEBサイトでよく見るハンバーガーボタンの見た目が簡単に作れます。

```react:src/components/Hamburger.astro
---
---
<div class="hamburger">
  <span class="line"></span>
  <span class="line"></span>
  <span class="line"></span>
</div>
```
### 2. ヘッダーコンポーネントでimport
```react:src/components/Header.astro
---
import Hamburger from './Hamburger.astro';
import Navigation from './Navigation.astro';
---
<header>
  <nav>
    <Hamburger />
    <Navigation />
  </nav>
</header>
```
### 3. ハンバーガーボタン、クリックした時に表示するメニューのCSSを実装
```css:src/styles/global.css
/* ナビゲーションのスタイル */
.hamburger {
  padding-right: 20px;
  cursor: pointer;
}

.hamburger .line {
  display: block;
  width: 40px;
  height: 5px;
  margin-bottom: 10px;
  background-color: #ff9776;
}

.nav-links {
  width: 100%;
  top: 5rem;
  left: 48px;
  background-color: #ff9776;
  display: none;
  margin: 0;
}

.nav-links a {
  display: block;
  text-align: center;
  padding: 10px 0;
  text-decoration: none;
  font-size: 1.2rem;
  font-weight: bold;
  text-transform: uppercase;
}

.nav-links a:hover, a:focus {
  background-color: #ff9776;
}

.expanded {
  display: unset;
}

@media screen and (min-width: 636px) {
  .nav-links {
    margin-left: 5em;
    display: block;
    position: static;
    width: auto;
    background: none;
  }

  .nav-links a {
    display: inline-block;
    padding: 15px 20px;
  }

  .hamburger {
    display: none;
  }
}
```
### 4. JavaScriptでイベントを実装
```javascript:src/scripts/menu.js
document.querySelector('.hamburger').addEventListener('click', () => {
  document.querySelector('.nav-links').classList.toggle('expanded');
});
```

### 5. .jsファイルをimport
そのままimportすると、ハンバーガーボタンをクリックしても何も起こらなくなってしまうので、JavaSctiptを動かすために`<script>タグ`をつけてimportします。
```javascript:src/layouts/BaseLayout.astro
// 省略
 <body>
    <Header />
    <h1>{pageTitle}</h1>
    <slot />
    <Footer />
    <script>
      import "../scripts/menu.js";
    </script>
  </body>
```
これで要件通り、ハンバーガーボタンを実装することができました。

## Reactで実装した場合
### 1. ハンバーガーコンポーネントを作成する

```react:src/components/Hamburger.tsx
import React from 'react';
import './Hamburger.css'; 

const Hamburger: FC = () => {
  return (
    <div className="hamburger">
      <span className="line"></span>
      <span className="line"></span>
      <span className="line"></span>
    </div>
  );
};

export default Hamburger;

```
### 2. ヘッダーコンポーネントでimport
1.で作ったコンポーネントをHeader.astroファイルでインポートします。
`client:load`はAstroで一部だけ動的なコンポーネントを埋め込みたい場合はに必要です。これをつけないと、クリックしてもイベントが起こらないので忘れないようにしましょう。
```react:src/components/Header.astro
---
import Hamburger from './Hamburger.astro';
---
<header>
  <nav>
    <Hamburger client:load/>
  </nav>
</header>
```

### 3. ハンバーガーボタン、クリックした時に表示するメニューのCSSを実装
```css:src/components/Hamburger.css
.hamburger {
  padding-right: 20px;
  cursor: pointer;
}

.hamburger .line {
  display: block;
  width: 40px;
  height: 5px;
  margin-bottom: 10px;
  background-color: #ff9776;
}

.nav-links {
  width: 100%;
  top: 5rem;
  left: 48px;
  background-color: #ff9776;
  display: none;
  margin: 0;
}

.nav-links a {
  display: block;
  text-align: center;
  padding: 10px 0;
  text-decoration: none;
  font-size: 1.2rem;
  font-weight: bold;
  text-transform: uppercase;
}

@media screen and (min-width: 636px) {
  .hamburger {
    display: none;
  }
```
### 4.ハンバーガーコンポーネントにロジックを追加
クリックした時に表示するメニューは、チュートリアルではNavigation.astroにファイルに実装していました。しかし、これはastroファイルなのでReactコンポーネント内でimportすることはできません。
NavigationコンポーネントをTSXファイルで実装しても良いのですが、簡略化のためハンバーガーコンポーネントの中に書いてしまいます。
```react:src/components/Hamburger.tsx
import React, { useState, type FC } from "react";
import './Hamburger.css'; 

const Hamburger: FC = () => {
  const [isOpen, setIsOpen] = useState(false);
  const toggleMenu = () => {
    setIsOpen(!isOpen);
  };

  return (
    <>
        <div className="hamburger" onClick={toggleMenu}>
          <span className="line"></span>
          <span className="line"></span>
          <span className="line"></span>
        </div>
        {isOpen && (
            // Navigationコンポーネントの内容
            <div class="nav-links">
              <a href="/">Home</a>
              <a href="/about">About</a>
              <a href="/blog">Blog</a>
            </div>
        )}
    </>    
  );
};

export default Hamburger;
```

## JavaScript/Reactで実装してみて感じた違い
これは私が実務でJavaScriptをほとんど使ってこなかったからかもしれませんが、JavaScriptの書き方は何をしているのかよくわからないと思いました。
慣れているからかもしれませんが、Reactで書いた方が圧倒的にどこで何が起きているかがわかりやすいと思いました。
ただ、Astroの場合はReactコンポーネントの中でAstroファイルをimportできなかったり、動的なコンポーネントを読み込みたい時は'client:load'をつけたりと、通常のReactとは異なる部分もあるため、注意が必要だと思いました。普通に書くのを忘れて、なんで動かないの！！となリました😇

## 終わりに
Astroは色々なファイルを読み込むことができて面白いです。何の言語でどのように書くのか、色々なパターンが使い分けられていいですね。
JavaScriptがわかりにくすぎてReactで書きましたが、多数決とったらどちらが好まれるんでしょうか…