React勉強しなおしてみた #1
本記事の内容
元JavaエンジニアがReactを再学習する記録。
React公式ドキュメントのLernページを学習材料とし、本記事内では下記セクションを学習する。
- インストール
- クイックスタート
経緯
Java + velocity or Java + SpringMVC + Tymeleafを主に使用して開発を行っていたが、フロントエンドにも力を入れるにあたってReactを勉強し始めた。
当時学習材料としたものは書籍「React.js&Next.js超入門」わかりやすく説明してある良書だが、内容が少し古い(クラスコンポーネントだったり...)のであまりおすすめはしない。
その後関数コンポーネント・HooksをQiitaやらzennやらで勉強した、がJavaを書く時の癖に引っ張られてReact推奨の書き方ができていなかった。
そのため最近のReactライクな書き方を覚えようと、一から勉強しなおすことにした。
React Lern
インストール
今回はNext.jsを利用。バージョンは20.11.1を使用。
npx create-next-app@latest
質問には「create-next-appで訊かれていること」を参考に、下記のように回答した。
What is your project named? ... react-learn
√ Would you like to use TypeScript? ... No / [Yes]
√ Would you like to use ESLint? ... No / [Yes]
√ Would you like to use Tailwind CSS? ... [No] / Yes
√ Would you like to use `src/` directory? ... No / [Yes]
√ Would you like to use App Router? (recommended) ... [No] / Yes
√ Would you like to customize the default import alias (@/*)? ... [No] / Yes
http://localhost:3000
で動作することを確認。
npm run dev
今回は作成時のHomeページをそのまま流用するため、mainの中身を空にしておく。これで準備完了。
export default function Home() {
return (
<>
<Head>
<title>React Learn</title>
<meta name="description" content="React Learn" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={`${styles.main} ${inter.className}`}>
/* この中をすべて削除 */
</main>
</>
);
}
クイックスタート
コンポーネントの作成とネスト
サンプル通り簡単なボタンコンポーネントを作成。
コンポーネント名は大文字で始まらなければならない。
export default function MyButton() {
return <button>I'm a button</button>;
}
Homeに追加。
export default function Home() {
return (
<>
<Head>
<title>React Learn</title>
<meta name="description" content="React Learn" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={`${styles.main} ${inter.className}`}>
<MyButton />
</main>
</>
);
}
Reactはこのコンポーネントを組み合わせてアプリを構築していく。
JSX でマークアップを書く
上記のようなHTMLに似た構文をJSX(Javascript XML)あるいはTSX(Typescript XML)といい、HTMLより構文が厳格。
複数のコンポーネントをreturnすることができないため、<div></div>
や<></>
等親要素でラップする必要がある。
この<></>
はFragmentといい、ラッパ用の要素を用いずに要素をグループ化する際に使用できる。
スタイルの追加
class``はJavaScriptの予約語のため、Reactではcssクラスを
classではなく
className```で指定する。それ以外は普通のHTMLでのページと同様にCSSを追加してスタイルを追加することができる。
データの表示
{}
を使用するとJSX内にJavaScriptの変数やコードを埋め込むことができる。
試しに先ほど作成したMyButtonコンポーネントを下記のように書き換えてみた。
style={{}}
はstyle={}
内にオブジェクト{}
を入れただけである。
export default function MyButton() {
const props = {
className: "normal-button",
backgroundColor: "blue",
color: "white",
title: "I`m a normal button",
};
return (
<button
className={props.className}
style={{ backgroundColor: props.backgroundColor, color: props.color }}
>
{props.title}
</button>
);
}
条件付きレンダー
条件によってレンダリングしたい内容を変えるには、通常のJavaScriptと同様にif文や三項演算子等を使用すればいい。
disable
プロパティによってレンダリング内容が変わるMyInputコンポーネントを作成してみた。disable=true
の場合はdiv
が、disable=false
の場合はinput
が表示される。
type Props = {
value: string;
disable: boolean;
};
export default function MyInput(props: Props) {
return <>{props.disable ? <div>{props.value}</div> : <input value={props.value} />}</>;
}
リストのレンダー
リストをレンダリングする際はfor
やmap()
などをを利用する。
下記はmap()
を使用してcolors
配列をリスト表示するサンプル。
type Color = {
id: string;
title: string;
};
const colors: Color[] = [
{ id: "red", title: "赤" },
{ id: "blue", title: "青" },
{ id: "yellow", title: "黄色" },
];
export function MyList() {
return (
<ul>
{colors.map((color) => {
return (
<li key={color.id} style={{ display: "flex", gap: 10 }}>
<div style={{ width: 20, height: 20, backgroundColor: color.id }}></div>
{color.title}
</li>
);
})}
</ul>
);
}
イベントに応答する
コンポーネント内にイベントハンドラを宣言することでclickやinput等のイベントに応答できる。
MyButtonをクリックするとアラートが表示されるよう変更。
export default function MyButton() {
const props = {
className: "normal-button",
backgroundColor: "blue",
color: "white",
title: "I`m a normal button",
};
function handleClick() {
alert("You clicked me!");
}
return (
<button
className={props.className}
style={{ backgroundColor: props.backgroundColor, color: props.color }}
onClick={handleClick}
>
{props.title}
</button>
);
}
画面の更新
コンポーネントに情報を記憶させて表示したい時、コンポーネントstate
を追加する。
例えば先に作ったMyInputはこのままだとvalue
がずっと「I`m a input」のため入力できない。これを入力可能にするためにstate
を追加していく。
まずstate
を管理するためのフックをimportする。
import { useState } from "react";
このuseState
を使用してstate
変数を宣言する。
const [value, setValue] = useState("I`m a input");
const [状態, 状態を変更するための関数] = useState(状態の初期値)
という形で宣言する。この場合、最初にMyInputが表示されるとき「I`m a input」となり、value
を変更したい場合setValue
を呼び出し新しい値を渡すことになる。
これでinputが機能するようになった。
import { useState } from "react";
type Props = {
disable: boolean;
};
export default function MyInput(props: Props) {
const [value, setValue] = useState("I`m a input");
function handleChange(value: string) {
setValue(value);
}
return (
<>
{props.disable ? (
<div>{value}</div>
) : (
<input value={value} onChange={(event) => handleChange(event.target.value)} />
)}
</>
);
}
フックの使用
先ほど使用したuseState
のようにuse
から始まる関数をフック(hook)と呼ぶ。useState
はReactが提供する組み込みのフックであり、他にも様々なフックが提供されている。(リファレンス)
また、独自のフックを作成することも可能。
フックはコンポーネントのトップレベルあるいは他のフック内でのみ呼び出しが可能であり、条件分岐やループ内で使用したい場合は、新しくコンポーネントとして抽出して配置する。
コンポーネント間でデータを共有する
先ほどはMyInput内で独立したvalue
を持っていたため、実際に入力されたMyInputのみvalue
が変更されていた。
これを他コンポーネントともデータを共有し、同時に更新されるよう変更するにはstate
を上位のコンポーネントに移動させる。
試しに複数のMyInputで同じ内容を表示するよう変更してみる。
まず、上位のコンポーネントであるHome
にstate
を移動し、value
とイベントハンドラを渡す。
// ...
import { useState } from "react";
export default function Home() {
const [value, setValue] = useState("I`m a input");
function handleChange(value: string) {
setValue(value);
}
return (
<>
<Head>
<title>React Learn</title>
<meta name="description" content="React Learn" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={`${styles.main} ${inter.className}`}>
<h1>React Learn</h1>
<MyInput value={value} disable={true} onChange={handleChange} />
<MyInput value={value} disable={false} onChange={handleChange} />
<MyList />
<MyButton />
</main>
</>
);
}
次にMyInputでvalue
とイベントハンドラを受け取れるようにpropsを変更。
type Props = {
value: string;
disable: boolean;
onChange: (value: string) => void;
};
export default function MyInput(props: Props) {
return (
<>
{props.disable ? (
<div>{props.value}</div>
) : (
<input value={props.value} onChange={(event) => props.onChange(event.target.value)} />
)}
</>
);
}
これで複数のコンポーネントでデータを共有できるようになった。
Discussion