はじめに
初めに勉強した時、もし私がこの記事に出会っていたら挫折はしなかったと断言できる記事に仕上げました。
Reactを学習したいけどなにからやればいいかわからない人や現在勉強していて躓いている人は是日最後まで読んで見て下さい。
私はReactをあらゆる教材(Reactチュートリアル(公式)・Udemy・Youtube)を使って学習しましたが幾度となく挫折して勉強を投げ出してきました。
しかし今回基本の「き」から勉強することで理解が深まり挫折しにくくなったと思いますのでその教材と学んだことをまとめます。
この記事を読むことで得られるもの
Reactってなに?
ひとことで言うと、
「ユーザーの見た目(UI)を作るための便利な道具がたくさん入った道具箱」。
さらにこの道具箱は、画面の一部だけを作ったり、効率よく動かしたりできる仕組みがそろっているから、大きなWebアプリを作るときにとても役立ちます
キーワード:「ユーザーの見た目」「道具箱」「画面の一部」「効率よく動く」
-
ユーザーの見た目
ユーザーがページにアクセスした際表示される画面。ホームページのトップページや詳細ページ全て、つまりサイト(サービス)の見た目。 -
道具箱
これは、Webアプリを開発しているとよく耳にするフレームワークというものです。
フレームワークとは、ライブラリ(便利なコードの集合体)よりも広い範囲をカバーする仕組みで、独自のメソッドや関数、ルール、構造などがひとまとめになった道具箱のようなものです。
この道具箱(フレームワーク)を使えば、ゼロから全部作らなくても、決まった流れにそって効率よくアプリを組み立てられます
補足:Reactは「ライブラリ」
結論フレームワークと思っていても問題ありません。
厳密にはUI構築のためのライブラリですが、周辺のルーティングや状態管理などのライブラリと組み合わせることで、フレームワークのように包括的に使われることが多いです。そのため、会話や記事の中では「フレームワーク」と呼ばれることもよくあります。
- 画面の一部
「画面の一部」というのは、たとえばヘッダー
,メインコンテンツ
,ナビメニュー
やサイドメニュー
それからお問い合わせフォーム
など、Webページにあるそれぞれの役割を持った部分のことを指します。
Reactではこれらのかたまり(部品)をコンポーネントと呼びます。
さらに細かく見ると、たとえばヘッダーの中にあるナビメニューも、Reactでは別のコンポーネントとして分けて扱います。
このように、画面をパーツごとに分けて、それぞれを部品のように作って組み合わせるのが、Reactの特徴です。
- 効率よく動く
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
に変換して処理されるindex.html<html> <body> <div id="root"></div> <script src="script.jsx" type="module"></script> </body> </html>
script.jsx{/* 以下の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>
/<>...</>
を使う必要があるscript.jsxreturn( {/* <></>は<React.Fragment></React.Fragment>のショートハンド(省略記法) */} <> <h1>Hello React</h1> <p>Reactは世界を変えた<p> </> )
- JSXは
-
propsとは
- 親コンポーネントから子コンポーネントにデータを渡す仕組み(バケツリレー)
- 渡すデータは、文字や数値、関数、state(状態)など、いろいろなものを含むことができる
propsの使用例script.jsx{/* AppコンポーネントからCardコンポーネントに値を渡している */} export default function App() { return ( <div> <Card image="https://myapp.example.png" name="risa" title="Frontend Engineer" /> </div> ); }
card.jsx{/* 渡ってきた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
script.jsx// MyBox親コンポーネン <MyBox> {/* 下記の<p>タグで囲まれている部分がchildren */} <p>こちらがChildren</p> </MyBox>
mybox.jsxfunction MyBox(props) { return ( <div style={{ border: "1px solid gray", padding: "10px" }}> {props.children} </div> ); }
-
{props.children}
は<p>こちらがChildren</p>
に該当する(分割代入を使って受け取っていない) - JSXでstyle属性にスタイルを書く時は
style={{}}
の形で書く
childrenプロップス使用例
script.jsxexport default function App() { {/* このコードでは <img>タグと<p>タグがchildrenに該当する*/} return ( <div> <Accordion title="ようこそ"> <img alt="" src="https://myapp.example.png" /> <p> 僕の名前はぴょん吉。友達になってくれたらうれしいな!! </p> </Accordion> </div> ); }
accordion.jsximport 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> ); }
arrowIcon.jsx{/* カスタム可能な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 に渡しているので、後からwidth
やfill
などを指定してカスタマイズ可能になっている
- アイコンをSVG(画像ではなくベクター)で表示するコンポーネント(
「SVGって何?」
SVG は Scalable Vector Graphics の略で、拡大してもキレイに表示される画像形式
React ではアイコンを SVG で作ることが多い
状態管理(state)とは?
-
Reactにおいて、コンポーネント状態は
state
によって管理されている -
state
を更新する関数はset〇〇
で定義される(例:count
を更新する関数はsetCount
) -
state
はpropsとして子コンポーネントに渡すことができるsample.jsximport {useState} from 'react'; const [state, setState] = useState(初期値)
-
useStateは、Reactの関数コンポーネント内でしか使えない。コンポーネントの外で使うと、Reactのルール違反でエラーが発生する
sample.jsx{/* useStateを関数コンポーネントの外で使ってしまっているためエラーが発生する */} const [count, setCount] = useState(0); function App() { const handleClick = () => { {/* ここでcountを更新しようとしても、useStateの呼び出し方が間違っているので上手くいかない */} setCount(count + 1); }; }
再レンダリングとは?
-
state
が更新されると、Reactはその変更を検知して コンポーネントを再レンダリング(再描画) する - 再レンダリングによって、画面上の表示が最新の
state
の値に基づいて更新される - 再レンダリングは必要な部分だけに限定され、効率的に実行される(Reactの仮想DOMの仕組みによるもの)
- 再レンダリングが発生すると、関数コンポーネントの本体(=関数自体)が再実行され、新しいJSXで画面表示を更新する
useState使用例
tab.jsx{/* 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
は状態を更新する関数。dispatch
にaction
を渡すとtodoReducer
が呼ばれて新しい状態が返される
- このコードでは、Reactの
-
handleAddTodo
(ToDoを追加するための関数)-
title
はuseRef.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(オブジェクトごと)リストから削除される
- 引数として削除したい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
コンポーネント内)dialog.jsxconst 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)
sample.jsxsetTodos( todos.map((todo) => todo.id === editId ? { ...todo, title: newTitle } : todo )
問題点
- ぱっと見何をしているか意図が読みづらい
- アクション名が無いため「なぜ?」がコードから読み取りづらい
- 一方こちらのコード(useReducer)
sumple.jsxcase "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の基礎になる部分です。これらを何度も読んで概念を落とし込める(基礎をつくれる)と今後の勉強が楽しくなると思います。
みなさんの学習の一助になれば幸いです。