📘

React勉強しなおしてみた #1

2024/02/26に公開

本記事の内容

元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の中身を空にしておく。これで準備完了。

src/pages/index.tsx
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>
    </>
  );
}

クイックスタート

コンポーネントの作成とネスト

サンプル通り簡単なボタンコンポーネントを作成。
コンポーネント名は大文字で始まらなければならない。

src/components/Button/MyButton
export default function MyButton() {
  return <button>I&apos;m a button</button>;
}

Homeに追加。

src/pages/index.tsx
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={}内にオブジェクト{}を入れただけである。

src/components/Button/MyButton
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が表示される。

src/components/Input/MyInput
type Props = {
  value: string;
  disable: boolean;
};
export default function MyInput(props: Props) {
  return <>{props.disable ? <div>{props.value}</div> : <input value={props.value} />}</>;
}

リストのレンダー

リストをレンダリングする際はformap()などをを利用する。
下記はmap()を使用してcolors配列をリスト表示するサンプル。

src/components/List/MyList
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をクリックするとアラートが表示されるよう変更。

src/components/Button/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変数を宣言する。

src/components/Input/MyInput
const [value, setValue] = useState("I`m a input");

const [状態, 状態を変更するための関数] = useState(状態の初期値)という形で宣言する。この場合、最初にMyInputが表示されるとき「I`m a input」となり、valueを変更したい場合setValueを呼び出し新しい値を渡すことになる。
これでinputが機能するようになった。

src/components/Input/MyInput
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で同じ内容を表示するよう変更してみる。
まず、上位のコンポーネントであるHomestateを移動し、valueとイベントハンドラを渡す。

src/pages/index.tsx
// ...
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を変更。

src/components/Input/MyInput
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