一部記事にはアフィリエイト広告が含まれています。

初心者エンジニア、「React」をゼロから学ぶ

kaede
kaede

この記事は、私のプログラミング学習の過程を記録しています。内容が必ずしも最新かつ正確であることを保証するものではないので、参考にする際は公式ドキュメントなどもご確認ください。

Reactの基礎

Reactとは?コンポーネントの書き方を紹介

ReactとはJavaScriptのライブラリである。
ライブラリ = 便利なツールがまとまったものであり、Reactを使うことで簡単に画面の作成できる。

Reactは、コンポーネント(部品)で構成されてる。
コンポーネントとは、JSXで記述されたマークアップを返すJavaScriptの関数である。

// ボタンに関するMyButtonコンポーネントを作成

function MyButton() {
  return (
  // returnの中身をJSXで記述
    <button>ボタン</button>
  )
}

JSXとは、JavaScriptで定義されたマークアップ構文であり、HTMLと似ている。
最終的には、ReactがJSXをJavaScriptに変換している。
(ブラウザはJavaScriptしか理解できない)

// JSXで記述したマークアップ
const element = <h1>Hello, world!</h1>;
// 変換後のJS(内部で自動的に変換されるから、覚えなくてOK)
const element = React.createElement('h1', null, 'Hello, world!');
ReactDOM.createRoot(document.getElementById("root")).render(element);

JSXの特徴として、{}を用いてJavaScriptコードや変数を埋め込むことができる。

function MyButton() {
  const buttonName = "ボタンをクリック";
  return (
    <button>{buttonName}</button>
  )
}

コンポーネントの呼び出しと実行

・Reactアプリが起動すると、まず index.js が実行される
ReactDOM.createRoot()により、最初にレンダリングする場所を指定
root.render(<App />) により、App コンポーネントがレンダリング(画面に描画)される
・該当する index.html<div id="root"></div>App コンポーネントがレンダリングされる

// src/index.js (エントリーポイント)

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";

// React 18 以降では createRoot() を使う
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
// public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>React App</title>
  </head>
  <body>
    // Reactがここにコンポーネントを描画
    <div id="root"></div>
  </body>
</html>

Reactのファイル名は、.js または .jsx → 一般的には .js が多く使われる

レンダリングとは?

レンダリング:Reactがコンポーネント(JSX)を解釈し、仮想DOMを生成して、どのように画面に表示するかを決定する処理

// 一番最初のレンダリングを指定

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

描画:仮想DOMを基に実際のDOM(HTML)を更新し、ブラウザに表示するプロセス

・ コンポーネントが呼び出された際に自動でレンダリングが行われる(関数コンポーネントの場合)
  → 自分でレンダリング処理の関数render()を書く必要がない
・ ReactでHookによりレンダリングが発生するとコンポーネント内の処理が上から順に実行される
JSXは「変わった部分だけ」を再レンダリングする

ReactでHookを使ってみよう

Hook(フック)とは、コンポーネントが状態を持ったり、特定のタイミングで処理を実行するための仕組み。

useStateで単一の状態を管理

例)クリックしたら数字が増えていくボタン

useStateを使うことで表示する値(状態)を管理することが可能。
状態が更新されると、自動で画面も更新(レンダリング)される
(setCount関数にてcountの値が更新されると、自動で表示される値{count}も更新される)

※ 再レンダリング発生時、JSXは「変わった部分だけ」を再レンダリングする

import { useState } from "react";

export default function Button() {
  const [count, setCount] = useState(0);
  function countUp() {
    setCount(count + 1);
  }
  return <button onClick={countUp}>クリックしてカウントアップ:{count}</button>;
}

count:現在の状態を保持
setCount:状態を更新する関数(引数に更新後の状態を入れる)
useState:初期値を設定(今回は初期値0)

✔️ 【補足】裏で行われる処理イメージ(※ 疑似コード)

function useState(initialValue) {
  let state = initialValue; //  初期値をセット

  function setState(newValue) {
    state = newValue; //  新しい値に更新
    render(); //  Reactに「再レンダリングしてね!」と伝える
  }

  return [state, setState];
}

✔️ 通常のJavaScriptとの違い

JavaScript は「変数が変わったこと」を 自動で検知できない
(手動で直接DOMを操作して、表示を更新することは可能)

let count = 0;

function countUp() {
  count = count + 1;
  console.log(count); // カウントは増えるが、画面には反映されない!
}

console.log("クリックしても反映しない›: " + count);

✔️ 【useStateの応用】2つのボタンの違い

パターン①:それぞれのButtonコンポーネントが、それぞれの状態を保持

import { useState } from "react";

export default function MyButton() {
  return (
    <>
      <Button />
      <Button />
    </>
  );
}

function Button() {
  const [count, setCount] = useState(0);
  function countUp() {
    setCount(count + 1);
  }
  return <button onClick={countUp}>クリックしてカウントアップ:{count}</button>;
}

パターン②:それぞれのButtonコンポーネントが、共通の状態を保持

MyButton内で状態を保持し、それぞれの子コンポーネントにデータを渡している。
クリックするとMyButton内で状態が更新され、それぞれの子コンポーネントとデータが連携する。
※親コンポーネントから子コンポーネントにデータを渡す方法は後で解説

import { useState } from "react";

export default function MyButton() {
  const [count, setCount] = useState(0);
  function countUp() {
    setCount(count + 1);
  }
  return (
    <>
      <Button onClick={countUp} count={count} />
      <Button onClick={countUp} count={count} />
    </>
  );
}

function Button({ onClick, count }) {
  return <button onClick={onClick}>クリックしてカウントアップ:{count}</button>;
}

✔️ setCount(count + 1);は非推奨

React の state 更新が非同期で行われるため、予期しないバグが起こる可能性があるから。

setCount((prev) => prev + 1); が推奨

useReducerで複数の状態を管理

useStateと比較したuseReducerの特徴

状態更新のロジックの分離と再利用性
→ 更新状態のロジックをコンポーネントの外で分離して記述するため、再利用しやすい
複数の状態変更パターンを一元管理
→ 複数の状態更新処理(例えば、増加・減少・リセット)をreducer内で簡単に管理できる
複数の状態を一つのオブジェクトで管理できる
 → 複雑な状態管理をよりシンプルに扱える

const [state, dispatch] = useReducer(reducer, initialState);

reducer:状態をどう変化させるかを定義する関数
initialState:最初の状態(オブジェクト等で定義)を格納している変数
state:現在の状態(オブジェクト等で定義)を格納している変数
dispatch:reducerを呼び出し状態を変更する関数

例)カウントアップ・カウントダウンボタンにより値を変化させる

import { useReducer } from "react";

const initialState = { count: 0 };
function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 }; // オブジェクトで返す必要あり
    case "decrement":
      return { count: state.count - 1 };
    default:
      return state;
  }
}

export default function CountUpDownButtons() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <>
      <button onClick={() => dispatch({ type: "increment" })}>
        クリックしてカウントアップ
      </button>
      <button onClick={() => dispatch({ type: "decrement" })}>
        クリックしてカウントダウン
      </button>
      <p>カウント値:{state.count}</p>
    </>
  );
}

【事前定義】
・初期状態を const initialState = { count: 0 }; で定義(オブジェクトで管理)
・クリックした時に実行したい処理内容をreducer関数として定義

【処理順序】
1. クリックしたら内部で定義されているdispatch関数を実行
  (dispatch関数の引数には、reducer関数の第二引数に入れるオブジェクトを記載)
2. dispatch関数によってreducer関数が実行され、stateに格納される状態が変化
(reducer関数で返されたstateが、state変数に格納される)
3. stateの更新により、<p>カウント値:{state.count}</p> がレンダリングされる

✔️ 【補足】裏で行われる処理イメージ(※ 疑似コード)

function useReducer(reducer, initialState) {
  let state = initialState;  // 初期状態を保持

  function dispatch(action) {
    // 1. reducerを呼び出し、状態を更新
    state = reducer(state, action);
    // 2. 新しいstateで再レンダリングをトリガー
    render();
  }

  return [state, dispatch];
}

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      return state;
  }
}

✔️ 処理を記述する位置

reducer 関数 と initialState は、コンポーネントの外に定義
reducer 関数がコンポーネントの中にあると、再レンダリングのたびに毎回新しい reducer 関数が作られ、パフォーマンス低下

useReducer はコンポーネントの中に書く
→ フックは関数コンポーネントの中でしか使えない

useEffectで副作用を管理

Reactのメインの処理 → 状態(state)の変更、描画
副次的な処理(副作用) → APIによるデータの取得、DOMの変更、タイマーの設定など

useEffect(() => {
  // 副作用を実行する処理
}, [依存配列]);

第一引数(関数): 副作用として実行したい処理を記述
第二引数(依存配列): 変数を設定し、指定した変数が変更されると、第一引数の副作用が実行される

例)初回のみデータを取得したい場合

import { useState, useEffect } from "react";

function Blog() {
  const [articles, setArticles] = useState([]);

  // useEffectでAPIからデータを取得
  useEffect(() => {
    fetch("https://api.example.com/articles")
      .then((response) => response.json())
      .then((data) => setArticles(data));
  }, []);  // 空の配列は、初回のレンダリング時だけ実行

  return (
    <div>
      <h1>記事一覧</h1>
      <ul>
        {articles.map((article) => (
          <li key={article.id}>{article.title}</li>
        ))}
      </ul>
    </div>
  );
}

初回レンダリング開始
・Reactが<h1>記事一覧</h1>と空の<ul>を描画

useEffect内の関数を実行
・APIによりデータを取得

画面更新
<ul>の中身を描画

✔️ Reactのレンダリングってなに?

レンダリングとは、Reactが画面に表示するために行う処理のこと
useEffect内の処理は、レンダリング後に実行される。

  1. なんらかの条件でレンダリング実行
  2. レンダリングにより第二引数(依存配列)内の変数が変更
  3. 第一引数(関数)の副作用を実行

React Hook Formでフォームを管理

React Hook Formは、フォームの状態管理やバリデーションチェックを行う。
useStateなどと異なり、外部ライブラリのためインストールが必要。

npm install react-hook-form

必要な関数やオブジェクトをインポートして使用

const { register, handleSubmit, formState: { errors } } = useForm();

register関数:「name要素に◯◯が入力された」という情報を、React Hook Formが管理するためのオブジェクトを返す関数

・フォーム要素に値が入力(変更)されるたびに実行される
・入力管理に必要なオブジェクトを戻り値として返す(onChangeonBlurvalue など)
・戻り値をinput内で展開してすることで、フォームの入力管理に必要な関数が用意される

<input {...register("name")} /> // register関数の実行 & 戻り値の展開
// 展開後のイメージ(これらがregisterの戻り値に入っているため、簡単に書くことができる)

<input 
  ref={inputRef} 
  name="name" 
  onChange={handleChange} 
  onBlur={handleBlur} 
  value={value} 
/>

handleSubmit関数:フォームの送信処理を簡単に行う関数

・送信ボタンが押されたタイミング(onSubmitの場合)で実行される
・register関数で登録された入力値を元に、裏でバリデーションチェックを行う
・バリデーションが成功すれば、引数であるonSubmit関数(データ送信処理など)を実行

<form onSubmit={handleSubmit(onSubmit)}> // onSubmit()は事前に定義しておく

formStateオブジェクト:フォーム全体の状態(入力内容、エラー、送信状態など)を格納する

formState.errors はバリデーションエラーの詳細情報を格納
バリデーションエラーが発生した場合、そのエラーメッセージを表示するために使用

{errors.name && <p>{errors.name.message}</p>}

フォームのバリデーションを追加

register にオプションを渡してバリデーションルールを設定
errors.name.message に「名前は必須です」というエラーメッセージが格納されている

<input 
  {...register("name", { required: "名前は必須です" })} 
  placeholder="名前" 
/>
{errors.name && <p>{errors.name.message}</p>}

✔️ レベル1:バリデーション不要なフォーム

import { useForm } from "react-hook-form";

function App() {
  const { register, handleSubmit } = useForm();

  const onSubmit = (data) => {
    console.log(data); // 本来はデータの送信などを記述
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("name")} placeholder="名前" />
      <button type="submit">送信</button>
    </form>
  );
}

export default App;

処理の順序

  1. input に値が入力される
  2. レジスター関数 register("name") が実行される
  3. レジスター関数の戻り値が展開される {...register("name")}
  4. 送信ボタンが押される
  5. handleSubmit 関数が実行される(今回はバリデーションチェックは行わない)
  6. handleSubmit の中で、onSubmit 関数が実行される

✔️ レベル2:バリデーションを追加

import { useForm } from "react-hook-form";

function App() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm();

  const onSubmit = (data) => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("name", { required: "名前は必須です" })} placeholder="名前" />

    /* nameフィールドにエラーがあればエラーメッセージを表示 */
      {errors.name && <p>{errors.name.message}</p>}

      <button type="submit">送信</button>
    </form>
  );
}

export default App;

✔️ 【重要】バリデーションチェックが行われるタイミング

・送信ボタンを押した時に errors にエラー情報が入る
・デフォルトの設定では、入力中にリアルタイムでエラーメッセージが表示されない
→ zodを使うことでリアルタイムで反映される
初心者エンジニア、TypeScriptの「Zod」をゼロから学ぶ

useCallbackで関数の再生成を防ぐ

useCallback:関数の再生成を防ぐためのフック
        無駄な計算や処理を減らし、パフォーマンスを最適化

  1. クリック処理などにより、useStateの値が変化
  2. 自動で再レンダリング = コンポーネント関数の再実行が発生
  3. コンポーネントで定義しているhandleClickなどの関数が再生成(再定義)される
    → useCallbackで、不要な関数の再生成を防ぐ

※ useCallback は「関数の定義」を再利用するだけで、「中身の処理」は毎回普通に実行される

// クリックにより発生する関数

 const handleClick = useCallback(() => {
   setCount((prev) => prev + 1);
}, []);

依存配列(第二引数):配列内の値が変わる時だけ新しい関数をつくる
           [count]countが変わると新しい関数が作られる

✔️ useCallbackあり/なしで比較

クリックのたびに、再レンダリングが発生しCounterコンポーネントが上から再実行される

// useCallbackなし

import { useState } from "react";

const Counter = () => {
  const [count, setCount] = useState(0);

  // 毎回新しい関数が作られる!
  const handleClick = () => {
    setCount(count + 1);
  };

  return <button onClick={handleClick}>カウント: {count}</button>;
};

export default Counter;
// useCallbackあり

import { useState, useCallback } from "react";

const Counter = () => {
  const [count, setCount] = useState(0);

  // 関数をメモ化(キャッシュ)
  const handleClick = useCallback(() => {
    setCount((prev) => prev + 1);
  }, []);

  return <button onClick={handleClick}>カウント: {count}</button>;
};

export default Counter;

親コンポーネントから子コンポーネントにデータを渡そう

【基本】子コンポーネントに値を渡す方法

✔️ 3つのパターンを攻略

パターン①:文字列を渡す

// 親コンポーネント

function App() {
  return <Greeting name="太郎" />;
}
// 子コンポーネント

function Greeting(props) {
  return <h1>こんにちは、{props.name}さん!</h1>;
}
  1. 属性として子コンポーネントに渡す値を定義する
  2. name="太郎"オブジェクトとして、子コンポーネントに渡される
  3. 子コンポーネントは、渡されたオブジェクトをpropsに代入する(受け取る)
  4. props.nameで、propsオブジェクトのnameプロパティの値を表示する
// オブジェクトをpropsで受け取るイメージ

props = { 
  name: "太郎",
}

パターン②:JavaScriptを渡す

JavaScript(数値や変数)で値を渡す場合は、{ } で囲む。
{{ name: “太郎”, id: 10 }} は、JavaScriptであるオブジェクト{ } を渡している。

// 親コンポーネント

function App() {
  return <Data size={5} person={{ name: "太郎", id: 10 }} />;
}
// 子コンポーネント

function Data(props) {
  return (
    <h1>こんにちは、{props.person.name}さん!</h1>;
    <p>計算:{props.size * 2}</p>
  )
}

パターン③:コンポーネントを渡す

親コンポーネントで子コンポーネントを呼び出すとき、propsで親から子にデータを渡す。
属性ではなく、タグ内にデータを入れて渡すことで、childrenとして子にデータを渡すことができる。
<Greeting> ここでchildrenに渡すデータを定義 </Greeting>

// 親コンポーネント

function App() {
  return (
    <Greeting name="太郎">好きな食べ物はりんごです。</Greeting>;
  );
}
// 子コンポーネント

function Greeting(props) {
  return (
    <div>
      <h1>私の名前は{props.name}です。</h1>
      <p>{props.children}</p> 
    </div>  
  );
}

// 出力結果

私の名前は太郎です。
好きな食べ物はりんごです。

【推奨】分割代入を使って書き換えよう

✔️ 【JavaScriptの復習】分割代入とは?

1つのオブジェクトや配列の中身を、それぞれの変数に分割して代入できる。
オブジェクトの name と age のプロパティ値が、それぞれ name 変数と age 変数に入る。
プロパティ名と同じ変数名に、対応するプロパティの値が入る

const { name, age } = { 
  name: "太郎",
  age: "18"
}

console.log(name); // "太郎"
console.log(age);  // 18

✔️ 分割代入で値を受け取る方法

オブジェクトを分割代入することで、オブジェクトのプロパティ値を引数に代入して利用可能。

// 親コンポーネント

function App() {
  return <Greeting name="太郎" age="18" />;
}
// 子コンポーネント

function Greeting({ name, age }) {
  return <h1>こんにちは、{age}歳の{name}さん!</h1>;
}

スプレッド構文(...)で値を渡す

✔️ 【JavaScriptの復習】スプレッド構文(...)とは?

配列やオブジェクトを展開することが可能。

// 配列を展開

const arr1 = [1, 2, 3];
const arr2 = [5, 6, 7];
const arr3 = [...arr1, 4, ...arr2];

console.log(arr2); // [1, 2, 3, 4, 5, 6, 7];
// オブジェクトを展開

const person = { name: "太郎", age: 18 }
const updatedPerson = { ...person, country: "Japan" };

console.log(updatedPerson); // { name: "太郎", age: 18, country: "Japan" }

✔️ スプレッド構文を使わない場合

// 親コンポーネント

function App() {
  return <Greeting name1="太郎" age1="18" name1="花子" age1="17" />;
}
// 子コンポーネント

function Greeting({name1, age1, name2, age2}) {
  return (
    <Hello name={name1} age={age1} />;
    <Goodmornig name={name2} age={age2} />;
  )
}
// 孫コンポーネント

function Hello({ name1, age1 }) {
  return <h1>こんにちは、{age}歳の{name}さん!</h1>;
}

function Goodmornig({ name2, age2 }) {
  return <h1>おはよう、{age}歳の{name}さん!</h1>;
}

✔️ スプレッド構文を使う場合

JSXの場合、{...obj}によってオブジェクトを props に変換することができる

// 親コンポーネント

function App() {
  const person1 = { name: "太郎", age: "18" };
  const person2 = { name: "花子", age: "17" };

  return <Greeting person1={person1} person2={person2} />;
}
// 子コンポーネント

function Greeting({ person1, person2 }) {
  return (
   <>
     <Hello {...person1} />     //  <Hello name="太郎" age="18" />
     <Goodmornig {...person2} />   //  <Hello name="花子" age="17" /> 
   </>
  )
}
// 孫コンポーネント

function Hello({ name, age }) {
  return <h1>こんにちは、{age}歳の{name}さん!</h1>;
}

function Goodmornig({ name, age }) {
  return <h1>おはよう、{age}歳の{name}さん!</h1>;
}

子コンポーネントから親コンポーネントにデータを渡すことはない

Reactでは、子から親に直接データを渡すことはない。
しかし、親から受け取った関数を実行することで、子が親の状態を変更することはある。

例)子コンポーネントで定義したボタンをクリックすると、親コンポーネントの値が変化

import { useState } from "react";

export default function ClickCount() {
  const [count, setCount] = useState(0);
  function countUp() {
    setCount(count + 1);
  }
  return (
    <>
      <Button countUp={countUp} />
      <p>{count}回クリックされました</p>
    </>
  );
}

function Button({ countUp }) {
  return (
    <>
      <button onClick={countUp}>クリックしてカウントアップ</button>
    </>
  );
}

✔️ 子コンポーネントで親コンポーネントの値を変更したいとき

  1. 親コンポーネントから子コンポーネントに関数を渡す(setCount)
  2. 子コンポーネント受け取った関数を実行し、状態を更新(count)
  3. 状態の更新により、再レンダリングが発生し親の値が変更される

jest.moc
reacthookform
TailAdmin
zod

kaede
kaede

参考文献について
この記事は、公式ドキュメントや他のウェブサイトを参考にしながら、プログラミング学習の内容をまとめています。特に、ChatGPT(OpenAI)の情報を多く取り入れており、自分の言葉でわかりやすく要約しています。

コメント