まえがき
以下の勉強会で使用したハンズオン資料を再編集したものです。
- 【ハンズオン】React × Next.js で開発を始めよう! (2021/05/19 19:30〜)
- 【ハンズオン】React × Next.js で開発を始めよう!|IT勉強会ならTECH PLAY[テックプレイ]
こちらの動画動画でも同様の内容を解説しています。(一部ソースコードに違いあり)
【ハンズオン】ReactとNext.jsをコードを書きながらサクッと理解する!
概念説明
React.jsとは
- ユーザインターフェース構築のためのJavaScript ライブラリ
- 宣言的なView
- コンポーネントベース
- 一度学習すれば、どこでも使える(サーバーサイド、React Nativeなど)
Next.js とは
- Reactを本番環境で利用するためのフレームワーク
- 本番環境に必要な設定が予め組み込まれている
- Webpack、TypeScript、多言語対応、画像の最適化、サーバーサイド、静的生成、SEOなど
- 環境構築に要する時間を短縮できる
ハンズオン
この資料のソースコードはこちらで公開しています
Next.jsアプリの環境構築
前提条件
- Node.js 10.13 or later
CLIから環境を構築
開発ツール導入済のテンプレートから環境環境を構築
$ npx create-next-app nextjs-react-training --example "https://github.com/redimpulz/nextjs-typescript-starter"
$ cd nextjs-react-training
サーバーの起動
$ yarn dev
導入済の開発ツール
- TypeScript
- ESLint
- Prettier
- VScode拡張
Reactの基礎を学ぼう
- コンポーネント
- props
- useState(ステートフック)
- useEffect(副作用フック)
コンポーネント
UIを再利用可能な部品として定義する
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
コンポーネントに動的に値を渡す
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(ステートフック)
コンポーネントに状態を持たせる
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(副作用フック)
コンポーネントで副作用を実行する
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からデータを取得
- 取得結果を画面に反映する
- ボタンをクリックして再取得
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の表示
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、その他独自のフック
- ただし、使う場面はあまり多くない