🔖

Reactナニソレおいしいの?というそこのあたなへ

に公開

はじめに

初めに勉強した時、もし私がこの記事に出会っていたら挫折はしなかったと断言できる記事に仕上げました。
Reactを学習したいけどなにからやればいいかわからない人や現在勉強していて躓いている人は是日最後まで読んで見て下さい。

私はReactをあらゆる教材(Reactチュートリアル(公式)・Udemy・Youtube)を使って学習しましたが幾度となく挫折して勉強を投げ出してきました。
しかし今回基本の「き」から勉強することで理解が深まり挫折しにくくなったと思いますのでその教材と学んだことをまとめます。

この記事を読むことで得られるもの

Reactってなに?

ひとことで言うと、
「ユーザーの見た目(UI)を作るための便利な道具がたくさん入った道具箱」。
さらにこの道具箱は、画面の一部だけを作ったり、効率よく動かしたりできる仕組みがそろっているから、大きなWebアプリを作るときにとても役立ちます

キーワード:「ユーザーの見た目」「道具箱」「画面の一部」「効率よく動く」

  • ユーザーの見た目
    ユーザーがページにアクセスした際表示される画面。ホームページのトップページや詳細ページ全て、つまりサイト(サービス)の見た目。

  • 道具箱
    これは、Webアプリを開発しているとよく耳にするフレームワークというものです。
    フレームワークとは、ライブラリ(便利なコードの集合体)よりも広い範囲をカバーする仕組みで、独自のメソッドや関数、ルール、構造などがひとまとめになった道具箱のようなものです。
    この道具箱(フレームワーク)を使えば、ゼロから全部作らなくても、決まった流れにそって効率よくアプリを組み立てられます

補足:Reactは「ライブラリ」
結論フレームワークと思っていても問題ありません。
厳密にはUI構築のためのライブラリですが、周辺のルーティングや状態管理などのライブラリと組み合わせることで、フレームワークのように包括的に使われることが多いです。そのため、会話や記事の中では「フレームワーク」と呼ばれることもよくあります。

  • 画面の一部
    「画面の一部」というのは、たとえばヘッダー,メインコンテンツ,ナビメニューサイドメニューそれからお問い合わせフォームなど、Webページにあるそれぞれの役割を持った部分のことを指します。
    Reactではこれらのかたまり(部品)をコンポーネントと呼びます。
    さらに細かく見ると、たとえばヘッダーの中にあるナビメニューも、Reactでは別のコンポーネントとして分けて扱います。
    このように、画面をパーツごとに分けて、それぞれを部品のように作って組み合わせるのが、Reactの特徴です。

<div align="center">
<img src="https://files.oaiusercontent.com/file-UJhTvMWVsF9hY1AybU8LeR?se=2025-05-13T03%3A23%3A57Z&sp=r&sv=2024-08-04&sr=b&rscc=max-age%3D604800%2C immutable%2C private&rscd=attachment%3B filename%3Dd31e501e-8050-4dd4-aa85-d1e6fbfcbd8d.webp&sig=z26TGtVqLJSaxFRjdKd8ME9%2BLsVKncDvg74fceWfHS0%3D" alt="属性" width="300">
<p>図1:ウェブページにおけるコンポーネントイメージ図</p>
</div>

  • 効率よく動く
    Reactにはウェブを効率的に動かすことのできる様々な仕組みがあります
  • ユーザーの入力をすばやく画面に反映させるため、状態stateというものでデータを管理しています
  • 画面の全体をまるごとリロード(再読み込み)するのではなく、値が変わった部分だけを自動的に更新してくれます。しかも、実際のHTMLを直接操作するのではなく、 「仮想DOM(Virtual DOM)」 という仕組みを使ってどこが変わったのかを効率的に検出・反映する。これがReactの「速さ」と「滑らかさ」の秘密です
  • 状態stateは親コンポーネントから子コンポーネントへ「props(プロップス)」という仕組みを使ってデータを渡すことができる。これによりデータの受け渡し、連携がスムーズに行える

React はよく SPA(Single Page Application)と呼ばれる形で使われる
SPAとは、ページ全体を何度もリロードせずに、必要な部分だけを切り替えて表示するWebアプリの構造のことでユーザーにとっても「ページ遷移しても読み込みが速い」「アプリのようにサクサク動く」といったメリットがあります

Reactの基本JSXとは?

  • JSX(JavaScript XML)はReactに使われる構文で見た目はHTMLに似ている(ほぼHTML)

  • JavaScriptの中にHTMLのような記述を埋め込むことができる記法

  • 実態はJavaScriptの構文として扱われ、Reactが裏側で自動的にReact.createElementに変換して処理される

    <html>
        <body>
            <div id="root"></div>
            <script src="script.jsx" type="module"></script>
        </body>
    </html>    
    
    {/* 以下の2行はReactのを使うための基本をインポート */}
    import React from "react";
    import { createRoot } from "react-dom/client";
    
    export default function App() {
    {/* 以下はJSX */}
        const lang = "React"
        {/* JSX内にJavaScriptの値を埋め込む場合は ${} ではなく {} を使う */}
        return <div className="hello">Hello {lang}</div>; 
    }
    
    {/* HTML側の<div id="root">にReactを表示する*/}
    export const root = createRoot(document.getElementById("root"));
    root.render(<App />);
    
    • createRoot(...)はReact18から導入されたレンダリングAPI(アクセスポイント)これによりReactのルート「最上位」を作成する
    • root.renderは先ほど作成したrootにReactコンポーネント<App />をレンダリングしている
    • <App />の中には通常、ページ全体の構成となるコンポーネント(例えば<Header /><Main />)が入る※ここでは省略
      JSXで注意すること
      • JSXは<img>タグ等のHTMLでは閉じタグが必要無い場合でも必ず" / "を書いて閉じる
        <img src="logo.png" alt="ロゴ" /> 
        
      • JSXではHTMLのように要素を並列して並べることはできない
      • 要素をならべる場合は<React.Fragment> / <>...</>を使う必要がある
        return(
        {/* <></>は<React.Fragment></React.Fragment>のショートハンド(省略記法) */}
            <>
                <h1>Hello React</h1>
                <p>Reactは世界を変えた<p>
            </>
        )
        

propsとは

  • 親コンポーネントから子コンポーネントにデータを渡す仕組み(バケツリレー)
  • 渡すデータは、文字や数値、関数、state(状態)など、いろいろなものを含むことができる
    propsの使用例
    {/* AppコンポーネントからCardコンポーネントに値を渡している */}
    export default function App() {
      return (
        <div>
          <Card
            image="https://myapp.example.png"
            name="risa"
            title="Frontend Engineer"
          />
        </div>
      );
    }
    
    渡ってきた props を分割代入を使って受け取り、利用する例
    {/* 渡ってきたpropsを分割代入を使って受け取り、利用する */}
    export function Card({ name, title, image }) {
      return (
        <div className="business-card">
          <div className="business-card-img-wrap">
            {/* 渡ってきたimage,nameを利用しsrcとalt属性に使用する */}
            <img src={image} alt={name} className="business-card-img" />
          </div>
          <div className="business-card-body">
            <div className="business-card-name">
              {/* 渡ってきたnameを表示する */}
              {name}
            </div>
            <div className="business-card-title">
              {/* 渡ってきたtitleを表示する */}
              {title}
            </div>
          </div>
        </div>
      );
    }
    

children propsとは

  • タグの中に書いた内容を「子要素(children)」として受け取る特別なprops

    // MyBox親コンポーネン
    <MyBox>
    {/* 下記の<p>タグで囲まれている部分がchildren */}
      <p>こちらがChildren</p>  
    </MyBox>
    
    function MyBox(props) {
        return (
            <div style={{ border: "1px solid gray", padding: "10px" }}>
                {props.children}
            </div>
        );
    }
    
    • {props.children}<p>こちらがChildren</p>に該当する(分割代入を使って受け取っていない)
    • JSXでstyle属性にスタイルを書く時はstyle={{}}の形で書く

    childrenプロップス使用例

    export default function App() {
    {/* このコードでは <img>タグと<p>タグがchildrenに該当する*/}
      return (
        <div>
          <Accordion title="ようこそ">
            <img alt="" src="https://myapp.example.png" />
            <p>
              僕の名前はぴょん吉。友達になってくれたらうれしいな!!
            </p>
          </Accordion>
        </div>
      );
    }
    
    import React from "react";
    
    import { ArrowIcon } from "./icons";
    
    export function Accordion({title,children}) {
      return (
        <details className="accordion-details" open>
          <summary className="accordion-summary">
            {title}
            <ArrowIcon />
          </summary>
          <div className="accordion-body">
            {/* childrenはscript.jsxの <img>タグと<p>タグが表示される */}
            {children}
          </div>
        </details>
      );
    }
    
    {/* カスタム可能なsvgアイコン */}
    export function ArrowIcon(props) {
      return (
    {/* propsは今回は何も渡されていないので、空のオブジェクト {} になる */}
    {/* 将来的に <ArrowIcon width="20" height="20" /> のように属性が渡されたとき、
        そのまま <svg> に適用されるように {...props} と書いている */}
        <svg viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg" {...props}>
          <path d="M240 80c0-17.7-14.3-32-32-32s-32 14.3-32 32V224H32c-17.7 0-32 14.3-32 32s14.3 32 32 32H176V432c0 17.7 14.3 32 32 32s32-14.3 32-32V288H384c17.7 0 32-14.3 32-32s-14.3-32-32-32H240V80z" />
        </svg>
      );
    }
    
    

全体構成と処理の流れ

  • App(アプリのメイン部分)
    • 親コンポーネント(App)から子コンポーネント(Accordion)を呼び出してtitleというpropsを渡している
    • <img><p>children として Accordion に渡される
  • Accordion(開閉できるコンポーネント)
    • <summary>,<details>を使って「開閉できるUI」を作っている
    • <details>開いた時の中身としてchildrenを表示している
  • ArrowIcon(矢印のアイコン。SVGで描かれている)
    • アイコンをSVG(画像ではなくベクター)で表示するコンポーネント(<svg> タグ中に path という線の形が指定されている)
    • props をそのまま {...props} で svg に渡しているので、後から widthfill などを指定してカスタマイズ可能になっている

「SVGって何?」
SVG は Scalable Vector Graphics の略で、拡大してもキレイに表示される画像形式
React ではアイコンを SVG で作ることが多い

状態管理(state)とは?

  • Reactにおいて、コンポーネント状態はstateによって管理されている

  • stateを更新する関数はset〇〇で定義される(例:countを更新する関数はsetCount

  • stateはpropsとして子コンポーネントに渡すことができる

    import {useState} from 'react';
    
    const [state, setState] = useState(初期値)
    
    
  • useStateは、Reactの関数コンポーネント内でしか使えない。コンポーネントの外で使うと、Reactのルール違反でエラーが発生する

    {/* useStateを関数コンポーネントの外で使ってしまっているためエラーが発生する */}
    const [count, setCount] = useState(0);
    
    function App() {
      const handleClick = () => {
        {/* ここでcountを更新しようとしても、useStateの呼び出し方が間違っているので上手くいかない */}
        setCount(count + 1); 
      };
    }
    
    

    再レンダリングとは?

    • stateが更新されると、Reactはその変更を検知して コンポーネントを再レンダリング(再描画) する
    • 再レンダリングによって、画面上の表示が最新のstateの値に基づいて更新される
    • 再レンダリングは必要な部分だけに限定され、効率的に実行される(Reactの仮想DOMの仕組みによるもの)
    • 再レンダリングが発生すると、関数コンポーネントの本体(=関数自体)が再実行され、新しいJSXで画面表示を更新する

    useState使用例

    {/* clsxは複雑なクラス名の条件分岐を簡潔に書けるライブラリ */}
    import clsx from "clsx";
    import React, { useState } from "react";
    import { createRoot } from "react-dom/client";
    
    export default function App() {
    {/* 今回は選ばれているタグをstateとする */}
      const [tab,setTab] = useState(0)
      const handleTab = (index) => () => {
        setTab(index);
      } 
    
      return (
        <div className="tab" role="tab">
          <div className="tab-list" role="tablist">
            <button
              className = {clsx({active: tab === 1})}
              onClick = {handleTab(1)}
            >タブ1</button>
            <button
              className = {clsx({active: tab === 2})}
              onClick = {handleTab(2)}
            >タブ2</button>
            <button
              className = {clsx({active: tab === 3})}
              onClick = {handleTab(3)}
            >タブ3</button>
          </div>
          {
            tab === 1 &&
              <div className="tab-panel">ここにタブ1のコンテンツが入ります。</div>
          }
          {
            tab === 2 &&
              <div className="tab-panel">ここにタブ2のコンテンツが入ります。</div>
          }
          {
            tab === 3 &&
              <div className="tab-panel">ここにタブ3のコンテンツが入ります。</div>
          }
        </div>
      );
    }
    
    export const root = createRoot(document.getElementById("root"));
    root.render(<App />);
    

    全体構成と処理の流れ

    • 選ばれているタグの番号をstate(状態)で管理している
      • useState(0)によって最初は何も選ばれていない(タブ0)状態から始まる
    • handleTab関数は、ボタンがクリックされた時に呼び出され、選ばれたタブ番号をstateにセットします
      • タブ番号はtabという変数で保持され、タブごとにtab === 1などの条件で中身を切り替えている
    • clsxライブラリを使うことで、タブの番号が一致しているボタンだけclassName= activeを追加し、スタイルの切り替えを簡潔に行っている
    • tabはReactのstateで管理されているため、値が変わるとReactが自動で再レンダリングを行い、表示が更新される

Hooksとは?

  • Reactバージョン18では、より効率的にレンダリングを行ったり、一時的な状態(state)を扱うための様々な機能が導入された。その中でも特に重要なのが 「Hooks(フック)」 という仕組みです
  • Hooksは関数コンポーネントの中で 状態管理や、Reactの機能を使えるようにする特別な関数のこと
  • Reactバージョン18以前はクラスコンポーネントでしかできなかったことがHooksによってシンプルな関数コンポーネントでもできるようになった

良く使われるHooksの種類と用途

Hooks 用途
useState 値(状態)を保存し変更されると再描画される。最も基本的な状態管理
useEffect 副作用(データ取得・タイマー・イベントリスナーなど)を扱う
useRef DOMの参照や、値を保持しつつ再レンダリングを避けたいところに使う
useContext グローバルな値(テーマ・ログイン情報など)を複数コンポーネント間で共有する
useReducer useStateよりも複雑な状態ロジック(条件分岐や状態の種類が多い場合)を管理する

Hooksの使い方

useReducer

  • Reducer関数ではよくswitch文を使って、アクションに応じた状態更新の処理が書かれる
    基本構文
function 状態更新関数(現在の状態, アクション) {
  switch (アクション.type) {
    case "アクションの種類":
      // アクションの種類に応じた状態更新の処理
      return 新しい状態;
    default:
      return 現在の状態;
  }
}

const [状態, dispatch] = useReducer(状態更新関数, 初期状態, 初期化関数);

useRducer使用例

export const todoReducer = (state, action) => {
  switch(action.type) {
    case "ADD" :
      const newData = {
        id: Date.now(),
        title: action.title
      }
      return [
        ...state,newData
      ]
    case "REMOVE":
      const filteredData = state.filter((todo)=>todo.id !== action.id)
      return filteredData
    case "UPDATE" :
      return state.map((todo) =>{
        if(todo.id === action.id) {
          return {...todo,title: action.title}
        }else {
          return todo
        }
      }) 
  }
};



export default function App() {
  const inputRef = useRef(null);
  const [id, setId] = useState(null);
  const [todos, dispatch] = useReducer(todoReducer, []);
  const handleAddTodo = () => {
    const title = inputRef.current.value;
    inputRef.current.value = "";
    dispatch({ type: "ADD", title });
  };
  const handleRemoveTodo = (id) => () => {
    dispatch({ type: "REMOVE", id });
  };
  const handleEditTodo = (id) => () => {
    setId(id);
  };
  const handleCloseDialog = () => {
    setId(null);
  };
  const handleUpdateTodo = (title) => {
    dispatch({ type: "UPDATE", id, title });
  };
  const currentTodo = todos.find((todo) => todo.id === id);

  return (
    <div>
      <div className="todo-header"></div>
      <div className="todo-container">
        <div className="todo-input-wrap">
          <input className="todo-input" ref={inputRef} type="text" />
          <button className="todo-add-btn" onClick={handleAddTodo}>
            +
          </button>
        </div>
        <ul className="todo-list">
          {todos.map((todo) => (
            <li key={todo.id}>
              {todo.title}
              <div className="todo-list__btn">
                <button
                  className="todo-remove"
                  onClick={handleRemoveTodo(todo.id)}
                >
                  削除
                </button>
                <button className="todo-edit" onClick={handleEditTodo(todo.id)}>
                  編集
                </button>
              </div>
            </li>
          ))}
        </ul>
      </div>
      {!!currentTodo && (
        <Dialog
          defaultTitle={currentTodo.title}
          onClose={handleCloseDialog}
          onUpdate={handleUpdateTodo}
        />
      )}
    </div>
  );
}
  • todoReducerは状態(ステート)を更新するためのReducer関数。switch文を使ってアクションの種類ごとに処理を分けている

    • ADD(追加)

      • 新しいTodoを追加する処理
      • Date.now()を使用し一意のidを作り、action.titleを使ってタイトルを設定する。それを現在のステート(ToDoの配列)の末尾に追加している
    • REMOVE(削除)

      • 指定されたidのToDoを取り除く
      • filterメソッドを使用してaction.idと一致しないものだけを残すことで、指定されたToDoを取り除く
    • UPDATE(更新)

      • 指定されたidのToDoのタイトルを更新する
      • mapメソッドで全てのToDoをチェックし、action.idと一致したToDoだけを更新する
      • 一致しない時はそのままToDoを返す
  • const [todos, dispatch] = useReducer(todoReducer, []);

    • このコードでは、ReactのuseReducerというフックを使って、状態変化を行っている
    • todosには現在の状態(ToDoの一覧)が入る変数初期値は空の配列([])になっています
    • dispatchは状態を更新する関数。dispatchactionを渡すとtodoReducerが呼ばれて新しい状態が返される
  • handleAddTodo(ToDoを追加するための関数)

    • titleuseRef.current.valueを使って入力欄(inputタグ)に入力された値を取得する
    • dispatch({ type: "ADD", title });によってADDアクションを発行する。これによりtodoReducerが呼び出され、新しいToDoが追加される
    • ToDoを追加した後、入力欄を空にするため、inputRef.current.value = ""とする
  • handleRemoveTodo(特定のToDoを削除するための関数)

    • 引数として削除したいToDoのidを受け取る
    • この関数は高階関数と呼ばれ実行時に関数を返す構造になっている
      これは、Reactのイベントハンドラで関数を実行する際にすぐ実行されず、クリックされた時、初めて実行されるようにするための工夫
    • dispatch({ type: "REMOVE", id });によってREMOVEアクションを発行する。これによりtodoReducerが呼び出され、該当するidを持つToDo(オブジェクトごと)リストから削除される

※高階関数とは:関数の中で別の関数を返す関数のこと。(関数を「扱う」関数
このようにすることで、ボタンをクリックしたときなどに「必要な引数を持った関数」を作り、それを遅延実行できる。

  • handleEditTodo(編集対象のTodoのidを記録する関数)
    • 引数として渡されたidを記録するための関数(高階関数)
    • setId(id)を実行することで「今から編集するToDo」のidをステートに保存する
    • 戻り値として関数を返す「関数を返す関数(クロージャー)」になっており、呼び出し方はonClick={handleEditTodo(todo.id)}のように使用する

※クロージャとは:外側のスコープの変数を記憶し続ける関数のこと。(外の変数を「覚えてる」関数
高階関数と併用されることが多く、両方の特徴を持つ場合もある(例:関数を返しつつ外部変数を保持)。

  • handleCloseDialog(編集モードを終了する関数)

    • 編集モードを終了するときに実行する関数
    • setId(null)によって、編集対象のidを空にする(編集状態を解除する)
  • handleUpdateTodo(特定のToDoを更新するための関数)

    • 引数として新しいタイトル(title)を受か取り、そのタイトルで該当するToDoを更新する
    • dispatch({ type: "UPDATE", id, title })によってUPDATEアクションを発行する。これによりtodoReducerが呼び出され、該当するidを持つTodoのタイトルを、渡されたtitleに更新する
    • idはステートに保存されている(handleEditTodoで設定した)idを使用する

    使用例(Dialogコンポーネント内)

    const handleUpdate = () => {
        const title = inputRef.current?.value || defaultTitle;
        onUpdate(title);
        onClose();
    };
    

どうしてuseStateではなくuseReducerを使うのか?

上記のuseReducerを用いてステートを管理しているコードをuseStateを使用して書くとどうなるのか?

export default function TodoApp() {
{/* 状態が増えるごとにuseStateが増えていく */}
 const [todos, setTodos] = useState([]);
 const [editId, setEditId] = useState(null);
 const inputRef = useRef();

{/* 状態更新ロジックが各関数にバラバラに記述される */}
 const handleAddTodo = () => {
   const title = inputRef.current.value;
   if (!title.trim()) return;
   const newTodo = {
     id: Date.now(),
     title: title,
   };
   setTodos([...todos, newTodo]);
   inputRef.current.value = "";
 };

 const handleRemoveTodo = (id) => {
   setTodos(todos.filter((todo) => todo.id !== id));
 };

 const handleEditTodo = (id) => () => {
   setEditId(id);
 };

 const handleCloseDialog = () => {
   setEditId(null);
 };

 const handleUpdateTodo = (newTitle) => {
 {/* setTodos()で直接更新すると意図が見えづらい */}
   setTodos(
     todos.map((todo) =>
       todo.id === editId ? { ...todo, title: newTitle } : todo
     )
   );
   handleCloseDialog();
 };

useStateの問題

  • 状態(ステート)が増えるとuseStateを記述する必要がある

    • 状態が増えるとコードが煩雑になる
  • 状態更新ロジックがバラバラに散らばる

    • 各操作(追加・削除・更新)の処理が、それぞれの関数内に分散している
    • 状態の変更がどこで何をしているか、把握しづらくなる
    • 結果としてコードの保守性が低下し、バグの発生しやすくなる
  • 状態更新の意図が読み取りづらい

    • 例えば下記のコード(useState)
    setTodos(
         todos.map((todo) =>
           todo.id === editId ? { ...todo, title: newTitle } : todo
         )
    

    問題点

    • ぱっと見何をしているか意図が読みづらい

    • アクション名が無いため「なぜ?」がコードから読み取りづらい

    • 一方こちらのコード(useReducer)

     case "UPDATE" :
          return state.map((todo) =>{
            if(todo.id === action.id) {
              return {...todo,title: action.title}
            }else {
              return todo
            }
          }) 
    

    利点

    • なんとなく値を更新していると分かる
    • アプリ全体の状態管理の見通しが良くなり、保守性が上がる

    ただし、useStateの概念を理解したうえで学習しないと理解できない

最後に

現在のモダン開発環境で一番使われることが多いReact。はじめにでも書きましたが私もReactでは何度も挫折してきました。初心者には理解できない箇所が随所にあります。
でもReactに憧れますよね?Reactでサクサク動くアプリを作りたいですよね?その気持ちはすごく分かります。
しかし、残念ながら、基礎がなければ家が建たないのと同じでJavaScriptの基本が曖昧ではReactを理解することは非常に困難です。そして、JavaScriptを理解したうえでReactの学びを始めたとしても、いきなりHooksの使い方やその他応用的な概念に触れてしまうと、正直訳がわからなくなり、私のように挫折する人が多いと思います。
そして、それは非常にもったいないことだと思います。
だからこそ、まずは基本(基礎)を理解することが何より大切だと考えています。
特に本記事のトピックReactってなに?から状態管理(state)とはまではReactの基礎になる部分です。これらを何度も読んで概念を落とし込める(基礎をつくれる)と今後の勉強が楽しくなると思います。
みなさんの学習の一助になれば幸いです。

使用教材

Discussion