【ハンズオン】ReactとNext.jsをコードを書きながらサクッと理解する！

## まえがき

以下の勉強会で使用したハンズオン資料を再編集したものです。

* [【ハンズオン】React × Next.js で開発を始めよう！ (2021/05/19 19:30〜)](https://ueqareer.connpass.com/event/212185/)
* [【ハンズオン】React × Next.js で開発を始めよう！｜IT勉強会ならTECH PLAY［テックプレイ］](https://techplay.jp/event/817327)

こちらの動画動画でも同様の内容を解説しています。（一部ソースコードに違いあり）

https://youtu.be/dlOa6GTYle8

# 【ハンズオン】ReactとNext.jsをコードを書きながらサクッと理解する！

## 概念説明

### React.jsとは

https://ja.reactjs.org/

- ユーザインターフェース構築のためのJavaScript ライブラリ
- 宣言的なView
- コンポーネントベース
- 一度学習すれば、どこでも使える（サーバーサイド、React Nativeなど）

### Next.js とは

https://nextjs.org/docs/getting-started

- Reactを本番環境で利用するためのフレームワーク
- 本番環境に必要な設定が予め組み込まれている
- Webpack、TypeScript、多言語対応、画像の最適化、サーバーサイド、静的生成、SEOなど
- 環境構築に要する時間を短縮できる

## ハンズオン

この資料のソースコードはこちらで公開しています

https://github.com/redimpulz/nextjs-react-training

## Next.jsアプリの環境構築

### 前提条件

- Node.js 10.13 or later

### CLIから環境を構築

開発ツール導入済のテンプレートから環境環境を構築

https://github.com/redimpulz/nextjs-typescript-starter

```bash
$ npx create-next-app nextjs-react-training --example "https://github.com/redimpulz/nextjs-typescript-starter"
$ cd nextjs-react-training
```

## サーバーの起動


```bash
$ yarn dev
```

[http://localhost:3000/](http://localhost:3000/)


### 導入済の開発ツール

- TypeScript
- ESLint
- Prettier
- VScode拡張

## Reactの基礎を学ぼう

- コンポーネント
- props
- useState（ステートフック）
- useEffect（副作用フック）

### コンポーネント

UIを再利用可能な部品として定義する

```tsx:index.tsx
import React from 'react';

// classコンポーネント
class HelloMessageClass extends React.Component {
  render() {
    return <div>Hello redimpulz</div>;
  }
}

// 関数コンポーネント
const HelloMessageFunction: React.FC = () => <div>Hello redimpulz</div>;

export default function Index() {
  return (
    <div>
      <HelloMessageClass />
      <HelloMessageFunction />
    </div>
  );
}
```

### props

コンポーネントに動的に値を渡す

```tsx:index.tsx
import React from 'react';

// propsの型定義
type Props = {
  name: string;
};

// ジェネリックスでpropsの型定義を指定
const HelloMessage: React.FC<Props> = (props) => <div>Hello {props.name}</div>;

export default function Index() {
  return (
    <div>
      {/* HTMLの属性のような形でコンポーネントに値を渡せる */}
      <HelloMessage name="redimpulz" />
    </div>
  );
}
```

### useState（ステートフック）

コンポーネントに状態を持たせる

```tsx:index.tsx
import React from 'react';

const HelloCounter: React.FC = () => {
  // コンポーネントに状態を持たせる
  const [count, setCount] = React.useState(0);

  return (
    <div>
      <div>{count}</div>
      <button onClick={() => setCount(count + 1)}>+</button>
      <button onClick={() => setCount(count - 1)}>-</button>
    </div>
  );
};

export default function Index() {
  return (
    <div>
      <HelloCounter />
    </div>
  );
}
```

### useEffect（副作用フック）

コンポーネントで副作用を実行する

```tsx:index.tsx
import React from 'react';
import Link from 'next/link';

const HelloCounter: React.FC = () => {
  const [count, setCount] = React.useState(0);

  // 副作用フック（マウント時）
  React.useEffect(() => {
    alert('mounted');
    // 副作用フック（アンマウント時）
    return () => alert('cleanup');
  }, []);

  // 副作用フック（依存の変更時）
  React.useEffect(() => {
    alert('counted');
  }, [count]);

  return (
    <div>
      <div>{count}</div>
      <button onClick={() => setCount(count + 1)}>+</button>
      <button onClick={() => setCount(count - 1)}>-</button>
      <div>
        <Link href="/">
          <a>back</a>
        </Link>
      </div>
    </div>
  );
};

export default function Index() {
  return (
    <div>
      <HelloCounter />
    </div>
  );
}
```

## 実践編：学んだことを活用してReactでアプリを実装

- ページアクセス時にAPIからデータを取得
- 取得結果を画面に反映する
- ボタンをクリックして再取得

```tsx:index.tsx
import React from 'react';

type Data = {
  message: string;
  status: string;
};

const HelloFetchImage: React.FC = () => {
  const [imageUrl, setImageUrl] = React.useState('');

  // データ取得とステートの保持
  const fetchData = async () => {
    try {
      const url = 'https://dog.ceo/api/breeds/image/random';
      const res = await fetch(url);
      const data: Data = await res.json();
      setImageUrl(data.message);
    } catch (error) {
      console.log(error);
    }
  };

  // 副作用フック（マウント時）
  React.useEffect(() => {
    fetchData();
  }, []);

  return (
    <div>
      {imageUrl && <img src={imageUrl} />}
      <div>
        <button onClick={fetchData}>fetch</button>
        <button onClick={() => setImageUrl('')}>clear</button>
      </div>
    </div>
  );
};

export default function Index() {
  return (
    <div>
      <HelloFetchImage />
    </div>
  );
}
```

### もう少し改善してみる

- 複数の画像表示に対応
- 初期の取得画像の数をpropsで設定できるように
- 取得する画像の数を入力で変更できるように
- loadingの表示

```tsx:index.tsx
import React from 'react';

// {
//   "message": [
//       "https://images.dog.ceo/breeds/lhasa/n02098413_16009.jpg",
//       "https://images.dog.ceo/breeds/appenzeller/n02107908_4092.jpg",
//       "https://images.dog.ceo/breeds/dalmatian/cooper2.jpg"
//   ],
//   "status": "success"
// }

type Data = {
  message: string[];
  status: string;
};

type Props = {
  defaultImageNums: number;
};

const HelloFetchImages: React.FC<Props> = ({ defaultImageNums }) => {
  const [imageNums, setImageNums] = React.useState<number | ''>(
    defaultImageNums
  );
  const [imageUrls, setImageUrls] = React.useState<string[]>([]);
  const [loading, setLoading] = React.useState(false);

  // データ取得とステートの保持
  const fetchData = async () => {
    if (!imageNums || !(imageNums >= 1 && imageNums <= 50)) {
      alert('please select 1...50 number');
      return;
    }
    setLoading(true);
    try {
      const url = `https://dog.ceo/api/breeds/image/random/${imageNums}`;
      const res = await fetch(url);
      const data: Data = await res.json();
      setImageUrls(data.message);
    } catch (error) {
      console.log(error);
    }
    setLoading(false);
  };

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (e.target.value) {
      setImageNums(parseInt(e.target.value));
    } else {
      setImageNums('');
    }
  };

  // 副作用フック（マウント時）
  React.useEffect(() => {
    fetchData();
  }, []);

  return (
    <div>
      {loading
        ? 'loading....'
        : imageUrls.map((x) => (
            <img key={x} src={x} style={{ width: 200, height: 200 }} />
          ))}
      <div>
        <input
          type="number"
          value={imageNums}
          onChange={handleChange}
          max={50}
          min={1}
        />
        <button onClick={fetchData}>fetch</button>
        <button onClick={() => setImageUrls([])}>clear</button>
      </div>
    </div>
  );
};

export default function Index() {
  return (
    <div>
      <HelloFetchImages defaultImageNums={20} />
    </div>
  );
}
```

## その他、学んでおきたいこと

- ref、context、memo、reducer、その他独自のフック
- ただし、使う場面はあまり多くない