React・Next.jsの勉強記録まとめ部屋
自分のReactの理解を深めるためにスクラップに学習記録を残していきます。
目標:Reactを使用したWEBサイトを作成できるようになる。
コピペじゃなく、ちゃんと理解して自分でコントロールしている感覚を掴むために基礎から徹底的に固める。細かいことでもアウトプットとして記録に残す。
なぜReactを学習するのか?
自分はマークアップエンジニアで基本はHTML,CSS,JavaScript,php,WordPressを使用した制作がメインだが、シンプルにReactやNext.jsを使用したWEBアプリケーション開発をしたい、楽しそう、が1番ではある。
その中でもReactを選ぶ理由は、
・世界的に人気のライブラリで需要が高い
・今後の仕事の幅を広げるため(最近はエンジニアとして企業で働いてみたいとも思ってきた)
・プログラミング言語を一つしっかり習得したかった
以上の理由と、Reactを学ぶ上でJavaScriptの基礎ができていないと活用しきれないので、JavaScriptの理解を深めて直近の実務の実装でも役立てることができるようにするためにReactを学ぶ。
勉強方法
・公式ドキュメントを読みつつハンズオン(日本語対応で読みやすい!)
・Udemyを見つつハンズオン
・自分のポートフォリオをReactで作成する、わからないところは調べつつまずは一つWEBアプリを開発してデプロイする
・勉強過程をZennのスクラップでアウトプットしていく、覚えたことは文章で整理して理解を深めていく。過程をZennに残す
React基礎
アロー関数の基礎
// 基本アロー関数で記述する
const fnArrow = (number) => {
console.log(number);
return number * 2;
};
// オブジェクトをそのまま返すときは()で挟む
const fnArrowObj = (number) => ({
result: number * 2,
});
console.log(fnArrow(2)); // 結果は4
console.log(fnArrowObj(2)); // 結果はresult: 4
オブジェクトリテラルとは
オブジェクトを直接記述する方法、{}の中に
{
キー: 値,
}
の形で記述する。
アロー関数でオブジェクトリテラルを記述する場合は、
丸括弧 () がないと、関数のブロック文として解釈されてしまう
// これは動作しません(エラー)
const getData = () => {
name: "John",
age: 30
};
// これが正しい書き方(丸括弧で囲む)
const getData = () => ({
name: "John",
age: 30
});
exportとimport
超重要、めっちゃ使う。
// 一つのファイルにつき一つだけデフォルトエクスポートを設定できる。
const funcB = () => {
console.log("funcB output");
};
export default funcB;
main.jsでインポート、関数名 + tabでimport文は補完される。ファイル名.jsの".js"がついていない場合は手動でつける必要がある。
いろんなexport方法だとしても、関数名を入力して補完してくれそう。
import funcB from "./module.js";
funcB();
コールバック関数
引数に渡された関数のこと
// 引数の中に関数が返ってくる
function print(callback) {
console.log(callback); // fnの関数が入っている
const result = callback(4); // callback(4)は渡された関数fn(4)として実行される
console.log(result); // 計算結果8が表示される
}
// numberの引数を初期値3と設定する
function fn(number) {
return number * 2;
}
// 動きなど、デバックしたい時にデバック行の前に挿入
// debugger;
// OKパターン🥰
// 関数のみ渡す
print(fn);
// NGパターン🤔
print(fn());
// これだとfn関数が "実行" されてしまい、実行結果が渡ってしまう。。。
// コールバックとして関数を渡すときは実行せずに ()をつけずに関数のみ渡すよう注意
DOMとイベントリスナー
// htmlのbutton要素を取得して変数buttonを定義
const button = document.querySelector("button");
// コンソールに"hello"を出力する関数
const helloFunc = (e) => {
console.log(e.target.textContent);
console.log("hello");
};
// buttonがクリックされた際のイベント
// あらかじめ定義した関数を実行
button.addEventListener("click", helloFunc);
// もしくは直接処理を記述
button.addEventListener("click", () => {
console.log("weeeee");
});
eとは何か
e はイベントオブジェクト (Event object) の略で、イベントに関する情報を含むオブジェクト。
eがないとどの要素がクリック等のイベントが発生したのかがわからなくなるので、選択された要素を取得するためにeは必要。
mapメソッド
mapメソッド
配列の各要素に対して同じ処理を行い、新しい配列を作作る。
valは配列の中身の一つづつの値
filterメソッド
条件に合う要素だけを抽出して新しい配列を作る。
const arry = [10, 20, 30, 40, 60, 80, 100];
// mapメソッドを使用した場合
// 新しい配列newArry2を定義
const newArry2 = arry.map((val) => {
console.log(val); // valはarryの配列の中身を一つづつ取り出している
return val * 2; // 取り出してきたval一つづつに対して*2を実行
});
console.log(newArry2);
// filterメソッドを使用して値が50以上のもののみをnewArry3に格納する
const newArry3 = newArry2.filter((val) => val > 50);
console.log(newArry3);
// 連結することも可能
const newArry2 = arry.map((val) => val * 2).filter((val) => val > 50);
// 配列arryの中で値を2倍し、かつ値が50以上のものを抽出してnewArry2を作成する。
分割代入
// オブジェクトの定義
const objAddress = { country: "Japan", state: "Tokyo", city: "Shinjuku" };
// 分割代入を使った書き方
const fnObject = ({ country, state, city }) => {
console.log(`国名: ${country}`);
console.log(`都市名: ${state}`);
console.log(`町名: ${city}`);
};
分割代入
配列またはオブジェクトから任意のプロパティを取り出して別の変数に代入などして使用できる。
・配列から取り出す場合は順番が大事、
・オブジェクトから取り出す場合はキーを一致させるようにする。
// オブジェクトの定義
const objAddress = { country: "Japan", state: "Tokyo", city: "Shinjuku" };
// 分割代入を使った書き方
const fnObject = ({ country, state, city }) => {
console.log(`国名: ${country}`);
console.log(`都市名: ${state}`);
console.log(`町名: ${city}`);
};
fnObject(objAddress);
// 例2
const user = {
name: "トム",
age: 27,
hobby: "サウナ",
};
const { name, age, hobby } = user;
console.log(`${name}さんは${age}歳で趣味は${hobby}です!`);
スプレッド演算子
配列、オブジェクトを新しい配列、オブジェクトとして作成する。
// 配列を準備
const partyPack = [3, 1, 4, 1, 5, 10, 2, 6];
const snack = [1, 2, 3, 15, 72];
// const result = Math.max(3, 1, 4, 1, 5, 10, 2, 6);
// スプレッド演算子、...の後に配列の変数を渡すと、配列の保持する要素が展開されて引数に渡される
const result = Math.max(...partyPack, ...snack); //数値の中で1番大きいものを取り出す
console.log(result);
// 元ある配列から、新しい配列を作成する際に使用する
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
let newArr = [...arr1]; // arr1を展開している
let newArr1 = arr1;
// newArr1に1000を追加すると、newArr1はもちろん配列に1000が加わるが、
// 参照元のarr1の配列にも1000が加わる
newArr1.push(1000);
console.log(newArr1);
console.log(arr1);
console.log("ここからはスプレッド演算子");
// だが、スプレッド演算子で新しく作成した配列newArrに300を加えて出力すると、
// newArrだけに変更が加えられて、参照元のarr1には追加されない。新しい配列として定義される
newArr.push(300);
console.log(newArr);
console.log(arr1);
// オブジェクトの場合
const obj = {
name: "Tom",
age: 22,
};
const newObj = { ...obj };
console.log(newObj);
const obj2 = { ...obj, mail: "email@gmail.com" };
console.log(obj2);
三項演算子
もし○○だったら△△、そうじゃなかったら□□」というif文を短く書く方法。 by Claude
めっちゃわかりやすかったのでそのまま引用。
// 三項演算子( ? : )
// if文を簡略化する際に使用
const a = true;
let resultA = a ? 10 : -1;
console.log(resultA);
// よく使う書き方
function getResult() {
return a ? "aはtrue" : "aはfalse";
}
console.log(getResult());
// 成績評価の例2
const score = 80;
let result = score >= 80 ? "合格" : "不合格";
console.log(`点数は${score}点なので${result}です。`);
falsyとtruthy
falsy → 真偽値に変換した際に"偽(false)"とみなされる値のこと。
truthy → それ以外
条件分岐の際に使用する。
falsyな値の一覧
false
0 (数字)
0n (big int)
"" (空文字)
null
undefined
NaN (Not a Number)
非同期処理 await / async
そもそもの話
・同期処理
コードが書いた順に実行される
・非同期処理
コードの書いた順番で実行されるとは限らない。callback関数とか、処理に時間がかかるものを非同期処理にする。処理重い部分で止まってしまってサイト読み込み遅くなったりパフォーマンスに影響するから。
Claudeさんの解説
日常生活での例え:
同期処理:
レストランで注文した順番通りに料理を作る
非同期処理:
レストランで注文を受けて、調理時間の違う料理は出来上がった順に提供する
resolve・・・解決
reject・・・拒否
Promise・・・約束、非同期処理を行うための記述
async・・・非同期の、「この関数は非同期処理を含みますよ」という宣言
await・・・待つ、非同期処理が完了するまで待つ、asyncの中でのみ使用可能
// ラーメンの注文処理をasyncで書いてみる
setOrder();
// 非同期で実行
async function setOrder() {
try {
// 注文が入る
console.log("ラーメン1丁入りました!");
// 注文を復唱する
await new Promise((resolve) => {
console.log("へい!ラーメンかしこまり!!!!");
resolve();
}, 1000);
// スープ完成
await new Promise((resolve) => {
setTimeout(() => {
console.log("スープができたぜ!");
resolve();
}, 3000);
});
// ラーメン完成!
await new Promise((resolve) => {
setTimeout(() => {
console.log("ラーメン完成!!");
resolve();
}, 2000);
});
// エラーの場合の出力
} catch (e) {
console.log("注文はお受けできません。。。", e);
}
}
React編
コンポーネント
画面の各構成要素をReactで定義したもの。
ヘッダーコンポーネントとか、サイドバーコンポーネントとか大きさ様々。
コンポーネントが集まってページになる。
なぜコンポーネントにするのか?
・再利用しやすい
・コードが読みやすくなる
・疎結合になってバグを減らせる
コンポーネントの定義
・先頭は大文字の関数で定義する。
<div id="app"></div>
<script type="text/babel">
const appEl = document.querySelector("#app");
const root = ReactDOM.createRoot(appEl);
// アロー関数でも定義できる
const Example = () => {
return (
<div>
<h2>私は関数コンポーネントですよ</h2>
<h3>ヘディング3</h3>
</div>
);
};
// 先頭が大文字じゃなく小文字だと、htmlのタグと認識されてしまうため
root.render(<Example />);
Reactプロジェクトの始め方
基本Viteを使用してプロジェクト立ち上げたほうが早いし推奨。
// 作業ディレクトリに移動する
cd /Users/作業ディレクトリ
// viteをインストール フォルダ名を決める
npm create vite@latest my-react-blog
選択画面では
React
JavaScript or TypeScript を選択
// 作成したディレクトリに移動
cd my-react-blog
// 新しいVS codeを立ち上げr
code .
// 用意されたnode_modulesをインストールする
npm install
// 開発環境を立ち上げる(NPM SCRIPTSのdevを押すでもOK)
npm run dev
// 自動的に立ち上がるローカルホストをクリックしてブラウザ立ち上げ
http://localhost:3000/
開発準備完了!
コンポーネントの分割とインポートエクスポート
エクスポート方法の違い
・名前付きエクスポート
その名の通り関数や変数に名前をつけてエクスポートして読み込む
関連する複数の機能を読み込むとき → 名前付きエクスポート
例)
Button.jsxファイルの中で
PrimaryButton
SecondaryButton
などある程度複数のコンポーネントを作成する場合に有効そう。
const List = () => {
return (
<ul>
<li>item-1</li>
<li>item-2</li>
<li>item-3</li>
<li>item-4</li>
<li>item-5</li>
</ul>
);
};
const PrimaryButton = () => {
return (
<>
<button>メインのボタン</button>
</>
);
};
const SecondaryButton = () => {
return (
<>
<button>2番目のボタン</button>
</>
);
};
export { List, PrimaryButton, SecondaryButton };
・デフォルトエクスポート
基本的に一つのファイルに一つのコンポーネントを作るので、デフォルトエクスポートで問題なさそう。インポート時もわかりやすい。
import "./Child.css";
// コンポーネント呼び出せばタブ補完で読み込んでくれた!
import { List, PrimaryButton, SecondaryButton } from "./List";
const Child = () => {
return (
<div className="component">
<h3>Hello Component</h3>
<List />
<div>
<p>メインのボタン</p>
<PrimaryButton /> // コンポーネントの呼び出し
<SecondaryButton /> // コンポーネントの呼び出し
</div>
</div>
);
};
// デフォルトエクスポート
export default Child;
// Childコンポーネントを下記ファイルから読みこむ、デフォルトエクスポートの読み込み
import Child from "./components/Child";
const Example = () => {
return <Child />;
};
export default Example;
フラグメントについて
基本的にはコンポーネントは一つのルート要素で囲まれている必要があるが、
<></>で囲むことで余計な<div>タグを生成しなくて済む。
import "./Child.css";
const Child = () => {
return (
<>
<div className="component">
<h3>Hello Component</h3>
</div>
<h3>Hello Fragment</h3>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Tempore ab
iure possimus quis asperiores placeat tempora ullam! Animi vero aliquid
repellat veritatis fugiat laboriosam enim quibusdam sint odit vitae
dolorem iste ea amet sapiente, suscipit commodi corrupti possimus
tempora dolores omnis illo optio! Ab laboriosam magni culpa neque veniam
mollitia?
</p>
</>
);
};
export default Child;
JSX
JSXはJavaScriptの中でHTMLが書ける。
変数・関数等を定義して{}で囲むことによってJavaScriptを評価することができる。
※注意点
JSXの {} 内には「値を返す式」だけが書ける。
それ以外はreturnの外に書く。
例)
値を返す式
/* POINT 式と文
式:何らかの値を返すもの(変数に代入できるもの)
文:変数宣言、for文、if文、switch文やセミコロンで区切るもの。
*/
import "./Child.css";
const Child = () => {
const hello = () => {
console.log("hello");
};
return (
<div className="component">
<h3>式と文</h3>
{hello()} {/* 関数を実行して返す */}
<p>trueならHelloを返す {true ? "Hello" : "Bye"}</p>{" "}
{/* 三項演算子を値で返す */}
<p>計算式は返す{1 * 20}</p>
</div>
);
};
export default Child;
import "./Expression.css";
const Expression = () => {
// 変数・関数を定義できる
const title = "Expression";
// 配列を定義して一つづつ展開してHTMLに表示できる
const arry = ["item1", "item2", "item3", "item4"];
// 関数も定義して使える
const hello = (arg) => `${arg} Function`;
const Jsx = <h3>Hello JSX</h3>;
return (
<div className={title.toLowerCase()}>
<h3>Hello {title}</h3> {/* 変数は{}で使用できる */}
<h3>{arry}</h3> {/* 変数arryは配列なので中身が一つづつ展開される */}
<h3>{hello("hello")}</h3>
{/* 画面上に表示されないコメントの書き方 */}
{<h3>Hello JSX</h3>}
{Jsx}
</div>
);
};
export default Expression;
props
・色違いのコンポーネントなどを作成する必要がない。重複を避けれる
・コンポーネントの再利用性が高まる
・親から子へ一方通行で値を渡す
・キーと値のペアで使用する
・子コンポーネントではprops.キー名で値を取り出せる
中身のデータと構造(コンポーネント)を分けて管理するのに重要そう。
同じコードを繰り返さないため、メンテナンス性のため、プログラミングってつくづく楽できるように作られたのだなと実感する。
// Example.jsx(親コンポーネント)
const Example = () => {
return (
<>
<Child /> {/* propsなし */}
<Child color="red" /> {/* colorというpropsを渡している */}
<Child color="blue" message="こんにちは" /> {/* 複数のpropsを渡せる */}
</>
);
};
// Child.jsx(子コンポーネント)
const Child = (props) => {
// propsは親から渡された値をすべて持つオブジェクト
console.log(props); // { color: "red" } や { color: "blue", message: "こんにちは" }
return (
<div className={`component ${props.color}`}>
<h3>{props.message}</h3>
</div>
);
};
分割代入も使用できる!
import "./Child.css";
// 分割代入も使用可能
const Child = ({ color, message, snack, juice }) => {
return (
<div className={`component ${color}`}>
<h3>Hello Component</h3>
<h3>{message}</h3>
<p>
今日のおやつは{snack}で、飲み物は{juice}です。
</p>
</div>
);
};
export default Child;
オブジェクトを展開して受け渡すこともできる。
{...array}を渡すとオブジェクトの中身が展開される。スプレッド演算子
import Child from "./components/Child";
const Example = () => {
const hello = (arg) => `Hello ${arg}`;
// オブジェクトを定義
const array = {
color: "red",
num: 124,
};
return (
<>
<Child
{...array}
name="ジョン"
age="27"
hobby="サウナ"
from="香川県"
fn={hello}
bool
obj={{
name: "Tom",
age: 18,
}}
/>
<Child
{...array}
name="真一郎"
age={10}
hobby="旅行"
from="島根県"
fn={hello}
obj={{
name: "emy",
age: 10,
}}
/>
</>
);
};
export default Example;
import "./Child.css";
// 分割代入も使用可能
const Child = ({ color, name, age, hobby, from, num, fn, bool, obj }) => {
return (
<article className={`component ${color}`}>
<h2>私の名前は{name}です。</h2>
<p>
年齢は{age}歳で、趣味は{hobby}です。
</p>
<p>出身地は{from}です!</p>
<h3>好きな素数は{num}です。</h3>
<p>{fn("の意味はこんにちわ!!")}</p>
<p>ブーリアンは定義されている?{bool ? "true" : "false"}</p>
<p>
名前は{obj.name}で、年齢は{obj.age}です。
</p>
</article>
);
};
export default Child;
特別なprops Childrenについて
コンポーネントを呼び出す際に、タグで挟むことによってその中身はChildrenとして使用できる。
import Profile from "./components/Profile";
import Container from "./components/Container";
const profile = [
{ name: "Takashi", age: 19, country: "Japan", color: "blue" },
{ name: "Jane", age: 28, country: "UK", color: "red" },
];
const Example = () => {
return (
<div>
{/* コンポーネントを呼び出す際に終了タグをつけて間にコンポーネントを挟んで使用することもできる */}
<Container title="Childrenとは?">
{/* タグで囲まれた中身がChildrenとされる */}
<Profile {...profile[0]} />
<Profile {...profile[1]} />
</Container>
</div>
);
};
export default Example;
疑問、コンポーネントの中で直接コンポーネントを呼び出しても結果同じなのでは?
Claudeに聞いたところpropsのバケツリレーと呼ばれるものがあり、
Example.jsxでオブジェクトを定義しているのに、Container.jsxの中でChildrenを使用せずに直接Profile.jsxを使うと、Example.jsxで定義したオブジェクトを使用することができなかった。。。
propsは一方通行、Example.jsxで定義したオブジェクトをExample.jsxの中で使用するためにChildrenを使用して、Container.jsxの中でChildrenを呼び出して使うことで、
Example.jsxで定義したオブジェクトを直接Profileコンポーネントに渡すことができます。
import "./Container.css";
import Profile from "./Profile";
// childrenをpropsで渡す
const Container = ({ title, children }) => {
return (
<div className="container">
<h3>{title}</h3>
{/* 👍childrenを使う */}
{children}
{/* 👎これじゃダメなの? */}
{/* <Profile {...profile[0]} /> */}
</div>
);
};
export default Container;
props受け渡しの流れ
Example.jsx(データ定義)
↓
Container.jsx(レイアウト)
↓
Profile.jsx(データ表示)
propsの重要なルール
・propsは一方通行
・propsは読み取り専用、書き換えできない
JSXと仮想DOMの関係
JSXにおいてHTMLはBabelによって関数形式に変換されて仮想DOMを出力している。
処理の流れ:
JSX → 関数形式に変換
関数形式 → 仮想DOMオブジェクト生成
仮想DOM同士を比較(差分検出)
必要な部分のみ実際のDOMを更新する。
これにより:
パフォーマンスの向上
メモリ使用の効率化
ユーザー体験の向上
に繋がる。Reactを使う上で重要なポイントな気がした。
// このようなHTMLは、
const sample2 = (
<div>
<h1>Hello!</h1>
<h2>Good to see you.</h2>
</div>
);
// JSXだとこのような形で関数形式で出力される
React.createElement("div", null),
React.createElement("h1", null, "Hello!"),
React.createElement("h2", null, "Good to see you.")
JSXとは
ReactによるJavaSciptの構文を拡張したもの。JSXはJSのオブジェクトに変
換される。
HTMLもタグ、クラス名、コンテンツの中身の形でオブジェクトに変換される。
stateとイベントリスナー
イベントリスナーとは
画面上でイベントが発生した時に実行したい関数を収納すること。
const Example = () => {
// クリックされた時に処理したい関数を定義する
const clickHandler = () => {
alert("クリックされましたぜ!");
};
const clickHandler2 = () => {
console.log("こっちはコンソールに出力!");
};
return (
<>
{/* 関数を即座に実行しないように注意!関数名の最後に()はつけない! */}
{/* onClickで関数を指定してクリックイベントを登録する */}
<button onClick={clickHandler}>クリックしてね</button>
<button onClick={clickHandler2}>クリックしてね2</button>
</>
);
};
export default Example;
色々なイベントハンドリング
import "./Example.css";
const Example = () => {
return (
<div>
<h3>コンソールを確認してください。</h3>
{/* クリックされた時 */}
<button className="c-btn" onClick={() => console.log("onClick検知")}>
ClickMe
</button>
<label>
入力値のイベント:
<input
type="text"
// フォーカスされた時
onFocus={() => console.log("onFocus検知")}
// フォーカスが外れた時
onBlur={() => console.log("onBlur検知")}
/>
</label>
<div>
<label>
入力値を取得:
{/* 値が変更された時、eで値を取得してその中身をコンソールに出力 */}
<input type="text" onChange={(e) => console.log(e.target.value)} />
</label>
</div>
<div
className="hover-event"
// マウスが要素に入った時
onMouseEnter={() => console.log("カーソルが入ってきました。")}
// マウスが要素から離れた時
onMouseLeave={() => console.log("カーソルが出ていきました。")}
>
ホバーしてね!
</div>
</div>
);
};
export default Example;
eがなんなのかわからなくなったのでおさらい
// 例:誰かに話しかけられた時の状況
const 話しかけられた = (状況) => {
console.log(状況.誰が); // 話しかけてきた人
console.log(状況.何を); // 言われた内容
console.log(状況.いつ); // 話しかけられた時間
}
// Reactでの例
const handleInput = (e) => {
console.log(e.target); // 入力された要素(input自身)
console.log(e.target.value); // 入力された値
console.log(e.timeStamp); // いつ入力されたか
}
Claudeがわかりやすかった。
eは、
イベントが発生した要素(自分自身)
イベントの詳細情報
イベント発生時の状況
を教えてくれる「情報パッケージ」のようなものです!
useStateについて詳しく
useStateについて、何ができるか?
画面を更新する際に、例えばinput等に入力された値ごと際レンダリングされてしまうと入力された値も更新されるので結局inputの中身が空っぽになってしまう。。。そこで入力された値をどこかに保存しておく必要がある!そこでuseStateを使う!
コンポーネントの状態を保持
状態の更新を管理
再レンダリング時も値を維持
stateはコンポーネント毎に保存される
useStateで値が更新されるとコンポーネントは再レンダリングされる
import { useState } from "react";
const Example = () => {
// useStateで値を保存して更新する
// [現在の値, 更新用の関数] = useState("初期値")
const [val, setVal] = useState(0);
return (
<>
<input
type="text"
onChange={(e) => {
// e.target.valueで更新された値を取得し、setVal()で値の状態を保持
setVal(e.target.value);
}}
/>
{/* 保存された値を参照 */}
<p>入力された値 = {val}</p>
</>
);
};
export default Example;
カウントアップ・ダウン処理の例
stateの更新はprevを使用して直前の状態に対して更新をするのが推奨。
import { useState } from "react";
const Example = () => {
const [count, setCount] = useState(0);
// カウントアップ関数
const countUp = () => {
setCount((prev) => {
return prev + 1;
});
console.log(count);
};
// カウントダウン関数
const countDown = () => {
setCount((prev) => {
return prev - 1;
});
};
// リセットして0にする
const reset = () => {
setCount(0);
};
return (
<>
<p>現在のカウント:{count}</p>
<button onClick={countUp}>+</button>
<button onClick={countDown}>-</button>
<button onClick={reset}>リセット</button>
</>
);
};
export default Example;
inputに入力した値をリアルタイムで更新する。
import { useState } from "react";
const Example = () => {
const personObj = { name: "Tom", age: 18 };
const [person, setPerson] = useState(personObj);
// 名前を変更
// 取得した値を使用するので引数にeを使用する
const changeName = (e) => {
setPerson({ name: e.target.value, age: person.age });
};
// 年齢を変更
const changeAge = (e) => {
setPerson({ name: person.name, age: e.target.value });
};
// リセット
const reset = () => {
setPerson({ name: "", age: "" });
};
return (
<>
<h3>Name:{person.name}</h3>
<h3>Age:{person.age}</h3>
<input type="text" value={person.name} onChange={changeName} />
<input type="number" value={person.age} onChange={changeAge} />
<button onClick={reset}>リセット</button>
</>
);
};
export default Example;
オブジェクトの中身を更新する際の注意点
// ❌ 良くない例(プロパティの欠落の可能性)
setPerson({ name: e.target.value, age: person.age });
// ✅ 良い例(全プロパティを確実に維持)
setPerson({ ...person, name: e.target.value });
変更しないオブジェクトの中の値を全て書いているとミスの原因にもなるし将来プロパティが追加された際も安全に管理することができるので、オブジェクトの展開は、スプレッド演算子を使用して展開し、新しい配列・オブジェクトとして定義する。
import { useState } from "react";
const Example = () => {
const personObj = { name: "Tom", age: 18 };
const [person, setPerson] = useState(personObj);
// 名前を変更
// 取得した値を使用するので引数にeを使用する
const changeName = (e) => {
// 1. ...person でuseStateで管理している既存のオブジェクトを展開
// 2. name: e.target.value で特定のプロパティだけを更新
// 3. 結果として新しいオブジェクトが作られる
setPerson({ ...person, name: e.target.value });
};
// 年齢を変更
const changeAge = (e) => {
setPerson({ ...person, age: e.target.value });
};
// リセット
const reset = () => {
setPerson({ name: "", age: "" });
};
return (
<>
<h3>Name:{person.name}</h3>
<h3>Age:{person.age}</h3>
<input type="text" value={person.name} onChange={changeName} />
<input type="number" value={person.age} onChange={changeAge} />
<button onClick={reset}>リセット</button>
</>
);
};
export default Example;
ステート更新の際の注意点
状態の更新が前の状態に依存する場合は、コールバック関数を使用
単純な値の置き換えの場合は、コールバック関数は不要
コールバック関数を使う場合は、必ずprev(または同様の引数)を使用する
import { useState } from "react";
const Example = () => {
const orderObj = { item: "apple", count: 10 };
const [order, setOrder] = useState(orderObj);
// アイテムの更新関数
const changeItem = (e) => {
setOrder((prev) => ({ ...prev, item: e.target.value }));
};
// オブジェクト内countのUp関数
const countUp = () => {
setOrder((prev) => ({ ...prev, count: prev.count + 1 }));
};
// オブジェクト内countのDown関数
const countDown = () => {
setOrder((prev) => ({ ...prev, count: prev.count - 1 }));
};
return (
<div>
<h3>Item:{order.item}</h3>
<h3>Count:{order.count}</h3>
<input type="text" value={order.item} onChange={changeItem} />
<button onClick={countUp}>+</button>
<button onClick={countDown}>-</button>
</div>
);
};
export default Example;
配列のステートの扱い方
新しい配列を定義するときはスプレッド演算子で元配列をコピーして新しい配列を定義してステートの更新に使用する。
import { useState } from "react";
const Example = () => {
const numArray = [1, 2, 3, 4, 5];
const [nums, setNums] = useState(numArray);
console.log(nums);
// クリックされると数字がシャッフルされるイベント
const shuffle = () => {
// スプレッド演算子で配列を展開して新しく配列newNumsをコピーして作成
const newNums = [...nums];
// pop()メソッドは配列の最後の値を取り出し、変数valueに保存する
const value = newNums.pop();
// unshift()メソッドで、取り出して保存したvalueを配列の先頭に追加する
newNums.unshift(value);
// setNums関数で新しい配列newNumsで更新する
setNums(newNums);
};
return (
<>
{/* 配列を{}で囲むと配列の中身が展開される */}
<h1>{nums}</h1>
<button onClick={shuffle}>シャッフル</button>
</>
);
};
export default Example;
ステートとコンポーネントの関係
keyについて
keyをつけておかないと、コンポーネントを使用する際に
下記状態の場合はステートの状態が共有されてしまう。。。
それぞれのコンポーネントを呼び出す際にkeyに任意の値をつけておけばReactは完全に別のコンポーネントとして認識するので、各コンポーネントは独立してステートの状態管理ができる。
大きいサービスになると特に重要な考え方になりそう。
タブ切り替えやフィルターとか実装するときに必要になりそう!
// ❌ keyなしの場合
{toggle ? (
<Count title="Aのカウント" /> // 状態が共有されてしまう
) : (
<Count title="Bのカウント" /> // 状態が共有されてしまう
)}
// ✅ keyありの場合
{toggle ? (
<Count key="A" title="Aのカウント" /> // 独立した状態
) : (
<Count key="B" title="Bのカウント" /> // 独立した状態
)}
ステートを保持する方法
ステートをpropseで渡すケース
・コンポーネントが消滅するケースがあるとき(トグルボタンで出し分けるときとか)
・特定のステートを複数の子コンポーネントで共有したいとき
下記コードの場合、
Countコンポーネントは表示用だけのコンポーネントとして作成し、
Exampleコンポーネントの中でステートの状態を一元管理する。
親コンポーネントから子コンポーネントへpropsで受け渡し、子コンポーネントで表示
親は状態管理に専念し、
子は表示することに専念する。
import { useState } from "react";
const Example = () => {
const [toggle, setToggle] = useState(true);
// 親コンポーネントでそれぞれのカウントのステートを定義
// カウントA用のステート
const [countA, setCountA] = useState(0);
// カウントB用のステート
const [countB, setCountB] = useState(0);
const toggleComponent = () => {
setToggle((prev) => !prev);
};
return (
<>
<button onClick={toggleComponent}>toggle</button>
{toggle ? (
// propsでtitle,count,setCountをCountコンポーネントに渡す
<Count key="A" title="A" count={countA} setCount={setCountA} />
) : (
<Count key="B" title="B" count={countB} setCount={setCountB} />
)}
</>
);
};
// Countコンポーネント
const Count = ({ title, count, setCount }) => {
const countUp = () => {
setCount((prevstate) => prevstate + 1);
};
const countDown = () => {
setCount(count - 1);
};
return (
<>
<h3>
{title}: {count}
</h3>
<button onClick={countUp}>+</button>
<button onClick={countDown}>-</button>
</>
);
};
export default Example;
useStateを使う理由まとめ
コンポーネントが再レンダリングされた際に、inputなどの入力された値ごと更新されてしまうので、値を保存する術がない、そんな時にuseStateで値をstateに保存することができる。
一度消滅したコンポーネントのstateの値はリセットされる、もしリセットされたくない場合は、親コンポーネントでstateを管理する。
mapを使用した配列リストの取り出し
配列を定義して、JSXの中で一つづつ取り出して使用する。
リストには必ずキーをつけて使用する。
keyをつけることで、どの要素に何が紐づいているかをReactに知らせて、変更分だけ特定して変更することができる。keyyがついていないと、配列に要素が追加された際に、どの要素が追加されたかを特定できないので、全ての要素に対して変更が加えられてしまう。
import Profile from "./components/Profile";
const Example = () => {
const persons = [
{
id: 1,
name: "Geo",
age: 18,
hobbies: ["運動", "ライブ観戦"],
},
{
id: 2,
name: "Geo",
age: 18,
hobbies: ["sports", "music"],
},
{
id: 3,
name: "Jon",
age: 18,
hobbies: ["sports", "music", "サウナ", "山菜取り"],
},
];
return (
<>
<ul>
{persons.map((person) => (
{/* 配列の中のidプロパティをkeyとして設定する */}
<li key={person.id}>
{/* 全てのプロパティを渡す */}
<Profile {...person} />
</li>
))}
</ul>
</>
);
};
export default Example;
const Profile = ({ name, age, hobbies, id }) => {
return (
<article>
<hr />
<h3>Name: {name}</h3>
<p>Age: {age}</p>
<div>
<p>Hobby:</p>
<ul>
{hobbies.map((hobby) => {
// 趣味の配列のリストのkeyはidとnameとhobbyを連結して出力されたものをkeyとして渡す
return <li key={`${id}-${name}-${hobby}`}>{hobby}</li>;
})}
</ul>
</div>
</article>
);
};
export default Profile;
配列のfilterメソッドの使い方
配列の中身をfilterしてmapで表示する、リアルタイムで検索結果が更新されるフィルタリング、実務で頻出。
import { useState } from "react";
const animals = ["dog", "cat", "rat", "dookey", "dyddy"];
const Example = () => {
// filter用のuseStateを定義
const [filterVal, setFilterVal] = useState("");
console.log(animals.filter((animal) => animal === "Dog"));
return (
<>
<h3>配列のフィルター</h3>
<input
type="text"
value={filterVal}
// 入力のたびに入力された値をsetFilterValで値を更新
onChange={(e) => setFilterVal(e.target.value)}
/>
<ul>
{animals
// 入力された文字が最初に現れる位置を返す、見つからなければ-1を返す
// animal配列の各要素に対して、filterValの文字列が含まれているかチェック、含まれている要素だけmapで表示される
// !== は「等しくない」を意味する
.filter((animal) => animal.indexOf(filterVal) !== -1)
.map((animal) => (
<li key={animal}>{animal}</li>
))}
</ul>
</>
);
};
export default Example;
indexOfについて
文字列検索メソッド、対象の文字列が何番目にあるかを返す、
存在しない場合は -1を返す(重要!)
// indexOf() は文字列検索メソッド
"dog".indexOf("d") // 結果: 0 (先頭にある)
"dog".indexOf("o") // 結果: 1 (2番目にある)
"dog".indexOf("g") // 結果: 2 (3番目にある)
"dog".indexOf("cat") // 結果: -1 (存在しない)
// !== は「等しくない」を意味する
animal.indexOf(filterVal) !== -1
// これは以下の意味:
// 「検索文字が見つからない(-1)」ではない
// → つまり「検索文字が見つかった」
条件分岐、三項演算子、&条件式など
import { useState } from "react";
const Example = () => {
const animals = ["Dog", "Cat", "Rat"];
const [filterVal, setFilterVal] = useState("");
return (
<>
<input
type="text"
value={filterVal}
onChange={(e) => setFilterVal(e.target.value)}
/>
<ul>
{animals
.filter((animal) => {
const isMatch = animal.indexOf(filterVal) !== -1;
console.log(animal.indexOf(filterVal));
return isMatch;
})
.map((animal) => (
// 三項演算子
// <li key={animal}>
// {animal + (animal === "Dog" ? " これは犬" : "")}
// </li>
// &&条件でtrueの場合だけ文字列を表示する、falseは表示されない
<li key={animal}>
{animal}
{animal === "Dog" && " これは犬"}
</li>
))}
</ul>
</>
);
};
export default Example;
Claudeの出力された実践的な使い分けの例がわかりやすかった。
// ✅ &&演算子: 条件に一致する時だけ表示
{isLoggedIn && <UserProfile />}
{isAdmin && <AdminPanel />}
{hasError && <ErrorMessage />}
// ✅ 三項演算子: 条件によって違う内容を表示
{isLoggedIn ? <UserProfile /> : <LoginButton />}
{isAdmin ? <AdminPanel /> : <UserPanel />}
{hasError ? <ErrorMessage /> : <SuccessMessage />}
リファクタリング
作成したコンポーネントを役割ごとにコンポーネント分割する。
最初からコンポーネント分けて書くのは最初は難しそう、
慣れるまではまず一つのファイルでコンポーネントを作成して、その後ファイル分割して各役割ごとにコンポーネントを分けていくのが良さそう、
#Example.jsx(全体の表示と配列、stateの管理をするコンポーネント)
import { useState } from "react";
import AnimalList from "./components/AnimalList";
import AnimalFilter from "./components/AnimalFilter";
const Example = () => {
const animals = ["Dog", "Cat", "Rat", "Cow", "Monkey"];
// 入力文字列用のfilter管理のuseState
const [filterVal, setFilterVal] = useState("");
// animalsのfilter用の変数
const filteredAnimal = animals.filter((animal) => {
const isMatch = animal.indexOf(filterVal) !== -1;
return isMatch;
});
return (
<>
<AnimalFilter filterState={[filterVal, setFilterVal]} />
{/* filteredAnimalでanimals配列の中身をfilterしたものをpropsでAnimalListコンポーネントに渡す */}
<AnimalList animals={filteredAnimal} />
</>
);
};
export default Example;
#AnimalList.jsx(受け取ったリストの表示をするコンポーネント、フィルターはExampleコンポーネントでしているので受け取るだけ)
import React from "react";
import AnimalItem from "./AnimalItem";
const AnimalList = ({ animals }) => {
return (
<>
{/* 三項演算子でアニマルが見つからない場合にはエラーメッセージを表示 */}
{animals.length === 0 ? (
<p>アニマルが見つかりません...ぴえん</p>
) : (
<ul>
{animals.map((animal) => {
return <AnimalItem animal={animal} key={animal} />;
})}
</ul>
)}
</>
);
};
export default AnimalList;
#AnimalItem.jsx(配列の中の各アイテムを表示するためのコンポーネント)
import React from "react";
const AnimalItem = ({ animal }) => {
return (
<>
<li key={animal}>
{animal}
{animal === "Dog" && "★"}
</li>
</>
);
};
export default AnimalItem;
#AnimalFilter.jsx(filter機能のコンポーネント)
import React from "react";
const AnimalFilter = ({ filterState }) => {
// 分割代入で受け取ったfilterStateを分解する
const [filterVal, setFilterVal] = filterState;
return (
<>
<input
type="text"
value={filterVal}
onChange={(e) => setFilterVal(e.target.value)}
/>
</>
);
};
export default AnimalFilter;
コンポーネント分割の考え方
親コンポーネントの役割
社長的役割、部下(中間管理コンポーネント・子コンポーネント)を管理して、state等の状態管理など、管理に専念する。
- アプリケーション全体の状態管理
- グローバルな設定の管理
- 認証状態の管理
- 中間管理コンポーネント・子コンポーネントへのデータ配布
機能単位の中間管理コンポーネントの役割
中間管理職的役割、部下(子コンポーネント)を管理して機能単位をまとめ上げる。
- 特定の機能ドメインの状態管理
- 関連する子コンポーネントの統括
- データの加工・更新処理
子コンポーネントの役割
社員的役割、専門的な役割(単一機能や表示)に専念する。
- 受け取ったpropsの表示に専念
- ユーザーインタラクションの処理
- 単一の責任に集中
コンポーネント分割の基準
- 再利用性
- 複数箇所で使用される可能性
- 汎用的な機能
- 責任の明確さ
- 単一の役割
- 明確な目的
- コードの量
- 100行を超える場合は分割検討
- 複雑なロジックは分離
- 状態管理の単位
- 関連する状態をまとめる
- propsのバケツリレーを防ぐ
メリット
- コードの保守性向上
- デバッグの容易さ
- テストのしやすさ
- チーム開発の効率化
- 再利用性の向上
変数の命名規則で気になったこと
下記コードのように大文字スネークケースで変数を定義する理由
不変性の明示
この値は変更されない(定数である)ことを示す
コード上で一目で定数だと分かる
グローバルな定数/設定値であることの表現
// 設定値の例
const API_URL = "https://api.example.com";
const MAX_ITEMS = 100;
const DEFAULT_SETTINGS = {
theme: "dark",
language: "ja"
};
// 選択肢の例
const CATEGORY_OPTIONS = ["食品", "衣類", "電化製品"];
const STATUS_CODES = {
OK: 200,
ERROR: 400
};
DOM操作
モーダルの中身の表示やトースト(保存しました!送信しました!とかの下から出るポップみたいなもの)の表示の際に有効的、親要素の制限を受けない。DOMの階層構造を超えることができる。
import { useState } from "react";
import { createPortal } from "react-dom";
import Toast from "./components/Toast";
const Toastportal = ({ children }) => {
const target = document.querySelector(".container.start");
return createPortal(children, target);
};
const Example = () => {
const [toastOpen, setToastOpen] = useState(false);
return (
<div>
<h3>トーストの作成(createPortal)</h3>
<div className="container start"></div>
<button
type="button"
onClick={() => setToastOpen(true)}
disabled={toastOpen}
>
トーストを表示する
</button>
{toastOpen && (
<Toastportal>
<Toast
visible={toastOpen}
handleCloseClick={() => setToastOpen(false)}
/>
</Toastportal>
)}
</div>
);
};
export default Example;
useRef
useRefとは?
再レンダリングを発生させずに値を保持する方法(これがuseStateと違う!)
DOMへ直接アクセスすることができる。
書き換え可能な値を .current プロパティ内に保持することができる「箱」のようなもの。useRef() を使うと、毎回のレンダーで同じ ref オブジェクトが返される。
useRefの主な特徴まとめ:
- DOM要素を取得して直接アクセスして何かしらメソッドなど処理ができる
- 値の保持(再レンダリングされても維持される)
- 更新しても再レンダリングが発生しない
- .currentプロパティで値にアクセスできる
- 初期値を設定可能
useStateとの違い
const Component = () => {
// ステート:値が変更されると再レンダリング
const [count, setCount] = useState(0);
// ref:値が変更されても再レンダリングされない
const countRef = useRef(0);
return (
<>
<p>State: {count}</p>
<p>Ref: {countRef.current}</p>
</>
);
};
// 1. useState を使うケース
const [value, setValue] = useState("");
- ユーザーに見せる値
- UIに反映させたい値
- コンポーネントの再レンダリングが必要な値
// 2. useRef を使うケース
const elementRef = useRef();
- DOM要素への直接アクセス
- 再レンダリングを避けたい値の保持
- UIに直接反映させない内部の値
実例
import { useRef } from "react";
import { useState } from "react";
const Case1 = () => {
const [value, setValue] = useState("");
// useRefを設定
const inputRef = useRef();
return (
<div>
<h3>ユースケース1</h3>
<input
type="text"
value={value}
// 作成したinputRefを指定
ref={inputRef}
onChange={(e) => setValue(e.target.value)}
/>
<button onClick={() => inputRef.current.focus()}>
インプット要素をフォーカスする
</button>
</div>
);
};
// 動画の再生停止切り替え
const Case2 = () => {
const [playing, setPlaying] = useState(false);
const videoRef = useRef();
return (
<>
<video
style={{ maxWidth: "100%" }}
ref={videoRef}
src="./sample.mp4"
></video>
<button
onClick={() => {
if (playing) {
videoRef.current.pause();
} else {
videoRef.current.play();
}
setPlaying((prev) => !prev);
}}
>
{playing ? "Stop" : "Play"}
</button>
</>
);
};
const Example = () => {
return (
<>
<Case1 />
<Case2 />
</>
);
};
export default Example;
forwardRefについて
Reactにおいてコンポーネントが ref を受け取って子コンポーネントに転送すること
覚えておくと便利なユースケース
スクロールアニメーション
フェードイン・アウト
ホバーエフェクト
モーダル・ポップアップのアニメーション
スライドショー・カルーセル
// 1. スクロールアニメーション
const ScrollReveal = forwardRef((props, ref) => {
return (
<div
ref={ref}
className="scroll-reveal"
>
{props.children}
</div>
);
});
// 使用例
const ScrollSection = () => {
const sectionRef = useRef();
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
sectionRef.current.classList.add('visible');
}
}
);
observer.observe(sectionRef.current);
return () => observer.disconnect();
}, []);
return (
<ScrollReveal ref={sectionRef}>
<h2>スクロールで表示</h2>
</ScrollReveal>
);
};
// 2. ホバーアニメーション
const HoverEffect = forwardRef((props, ref) => {
return (
<div
ref={ref}
className="hover-element"
onMouseEnter={() => {
ref.current.animate([
{ transform: 'scale(1)' },
{ transform: 'scale(1.1)' }
], {
duration: 300,
fill: 'forwards'
});
}}
>
{props.children}
</div>
);
});
問題解決の考え方、エラーとか
console.log()で確認する
コンソール出力などで各イベントが発火しているか?
コンソールログの置く場所を変えていって原因箇所を絞っていくイメージ。
関数の中においてどこで、止まっている?、値は更新されている?レンダリングされている?などの形で特定していって、Google検索やチャッピーに聞いて解決していくイメージ。
debugger;を挿入して確認する
処理を止めたい箇所に
debugger; // この部分で更新が止まる
を挿入して、コード止めながら処理を一つづつ確認する、その際に変数が保有している値とか、propsで渡ってきている値などを確認して意図したものになっているかどうかを確認する。
プログラミング型
-
手続き(命令)型プログラミング
従来通りの上から実行されるプログラミング手法 -
関数型プログラミング
手続き型の制御をなるべく関数に分離し、やりたいことに集中できるようにするプログラミング手法
メリット
- コードの可読性の向上、やりたいことがわかりやすい
- 拡張性・再利用性の向上
- テスト性の向上
- モジュール化の向上
- Tree Shaking(本番環境で無駄なコードをなくす)
重要なキーワード
- 値の状態管理と処理を分離する
- 純粋関数(副作用を排除する)特定の入力には特定の出力を返す
- 不変性 一度設定した値は書き換えない
useReducerとは
useStateの書き換えに使用する。
useStateとuseReducerの違い
- useStateとは
単語の意味
文字通り「状態(state)を使用する」という意味
Reactにおける最も基本的な状態管理のフック
機能の役割
状態の更新の仕方は利用側に託す
コンポーネント内で変更可能な状態を保持
状態の更新はsetState関数を通じて直接的に行う
状態が更新されると、自動的に再レンダリングが発生
擬人化すると
👍 「俺は状態だけ管理するぜ!はい、状態をどうぞ!」
🔄 更新方法は自由
📝 使用側で柔軟に実装
🎯 シンプルな用途に最適
- useReducerとは
単語の意味
「reducer(削減する人)を使用する」という意味
複数のアクションを集約して状態を管理するフック
機能の役割
状態の更新の仕方も状態側で管理する
複雑な状態ロジックをreducer関数にまとめる
dispatch関数を通じて「アクション」を発行
アクションに応じて状態を更新
擬人化すると
👔 「私は厳格なので更新方法まで管理します。更新は以下の方法でお願いします」
🔒 更新方法を厳格に管理
📋 決められたアクションのみ許可
🏢 大規模な状態管理に最適
大規模アプリケーションで複数人で開発する際はuseReducerを使用した方が使用側も更新方法に則って使用すればいいのでバグが生まれにくいし、見通しも良くなる。
シンプルな状態管理ならuseStateが良さそう。
useContext
親→子→孫とコンポーネントへpropsを受け渡していくと、
propsのバケツリレーが発生してしまう。それを防ぐためにあるのがuseContext
親でuseContextを定義して、受け渡したい子・孫コンポーネントで読み込むことで値を使用できる。
アプリケーション全体でstateを共有したい場合に有効。
Providerは「提供者」の役割
配下のコンポーネントすべてに値を提供する
複数の値を配列やオブジェクトとして渡せる
#Example.jsx
import { createContext, useState } from "react";
import Child from "./components/Child";
import OtherChild from "./components/OtherChild";
export const MyContext = createContext();
const Example = () => {
// 更新用関数をuseStateで作成
const [value, setValue] = useState(0);
return (
// useContextを使用し、useStateを子に渡すために全体をContext.Providerでラップする。これでこの中のコンポーネントは
// valueの名前のpropsに設定された値が全体で使用可能になる。
<MyContext.Provider value={[value, setValue]}>
<Child />
<OtherChild />
</MyContext.Provider>
);
};
export default Example;
#GrandChild.jsx
import { useContext } from "react";
import { MyContext } from "../Example";
const GrandChild = () => {
// useContextを使用して、MyContextに指定されたuseStateのvalueの値を呼び出して使用することができる。
const [value] = useContext(MyContext);
return (
<div style={{ border: "1px solid black" }}>
<h3>孫コンポーネント</h3>
{value}
</div>
);
};
export default GrandChild;
#OtherChild.jsx
import { useContext } from "react";
import { MyContext } from "../Example";
const OtherChild = () => {
// 分割代入で1番目だけ取得する場合には0番目の引数を入力せずに,で省略可能
const [, setValue] = useContext(MyContext);
// 更新用関数、setValueをとってくるのはMyContextで指定したExample.jsxから取ってきている。
const clickHandler = (e) => {
setValue((prev) => prev + 1);
};
return (
<div>
<h3>他の子コンポーネント</h3>
<button onClick={clickHandler}>+</button>
</div>
);
};
export default OtherChild;
contextをファイル分割して各フォルダで読み込んで使用する
頻繁に更新されない状態の管理(ダークモード、ユーザーのログイン状態)をアプリ・サイト全体で管理する際に専用ファイルに分割しておくことで見通しが良くなる。
ログイン状態、テーマカラー、言語設定などで使用できそう。
#Example.jsx
import "./Example.css";
import Header from "./components/Header";
import Main from "./components/Main";
import { ThemeProvider } from "./context/ThemeContext";
const Example = () => {
return (
<ThemeProvider>
{/* ↓↓↓ これらが全て children として扱われる ↓↓↓ */}
<Header />
{/* useThemeが使える */}
<Main />
{/* useThemeが使える */}
{/* ↑↑↑ これらが全て children として扱われる ↑↑↑ */}
</ThemeProvider>
);
};
export default Example;
#ThemeContext.jsx
import { useContext, useState, createContext } from "react";
export const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => {
// themeのstate管理
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={[theme, setTheme]}>
{children}
{/* ここでExample.jsxから渡された要素を展開,Header.jsxとかMain.jsxとか */}
</ThemeContext.Provider>
);
};
export const useTheme = () => useContext(ThemeContext);
#Header.jsx
import { useTheme } from "../context/ThemeContext";
const Header = () => {
// 定数THEMES(色を定数で管理する)
const THEMES = ["light", "dark", "red"];
const [theme, setTheme] = useTheme();
// ラジオボタンの切り替えイベント
// 選択されたvalueの値にthemeを更新する
const changeTheme = (e) => setTheme(e.target.value);
return (
<header className={`content-${theme}`}>
{THEMES.map((themeOption) => {
return (
<label key={themeOption} htmlFor={themeOption}>
<input
type="radio"
value={themeOption}
id={themeOption}
checked={theme === themeOption}
onChange={changeTheme}
/>
{themeOption}
</label>
);
})}
</header>
);
};
export default Header;
#Main.jsx
import { useTheme } from "../context/ThemeContext";
const Main = () => {
const [theme] = useTheme();
return (
<main className={`content-${theme}`}>
<h1>テーマの切り替え</h1>
</main>
);
};
export default Main;
useEffectについて
useEffectは副作用を扱うためのもので、データフェッチング、購読の設定、手動のDOM操作などに使用します
依存配列によって実行タイミングを制御できる。基本は[]空配列で、コンポーネントの初回レンダリング時に1回だけ実行することが多い。
useEffect(() => {
console.log("useEffect is called");
window.setInterval(() => {
setTime((prev) => prev + 1);
}, 1000);
// 空配列は依存配列、配列に含めたステートが更新されると、コールバック関数が再実行される。
// 空配列を入れる理由は、依存しているステートは無いとするため。よってコンポーネントが読み込まれた際1回だけ実行する
}, []);
クリーンアップ関数
タイマーなどの常に動き続ける関数を作成した場合、関数が設定されたままコンポーネントが消滅する(アンマウント)などの状況の際、裏で関数が動き続ける = リソースが無駄に消費される = パフォーマンス低下・バグの原因になるため、コンポーネント消滅時に終了処理を記述することができる。
基本書いておいた方がいい。
[] → コンポーネントの初回マウント時のみ
useEffect(() => {
const intervalId = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
return () => clearInterval(intervalId);
}, []); // コンポーネントのマウント時のみ実行
[依存配列] → 依存配列が更新されるたびに実行
const [delay, setDelay] = useState(1000);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(prev => prev + 1);
}, delay);
return () => clearInterval(intervalId);
}, [delay]); // delayが変更されたときに再実行
注意:クリーンアップ処理は、依存配列の値が変更され、次のuseEffect実行される前にも実行される。
第二引数が空配列の場合
useEffect発火→メイン処理実行→アンマウント→クリーンアップ関数実行→終了
第二引数が依存配列の場合
useEffect発火→メイン処理実行→依存配列に設定した値で{state更新→クリーンナップ関数実行→メイン処理実行} → state更新→{クリーンアップ関数実行→メイン処理実行} ... これがstate更新のたびに繰り返しされる。
useEffect(() => {
// メインの処理(副作用)
// クリーンアップ関数を返す
return () => {
// クリーンアップ処理
};
}, [依存配列]);
具体例)
import { useEffect, useState } from "react";
const Example = () => {
const [isDisp, setIsDisp] = useState(true);
return (
<>
{isDisp && <Timer />}
<button onClick={() => setIsDisp((prev) => !prev)}>トグル</button>
</>
);
};
const Timer = () => {
const [time, setTime] = useState(0);
useEffect(() => {
// console.log("init");
let intervalId = null;
intervalId = window.setInterval(() => {
setTime((prev) => prev + 1);
}, 1000);
// クリーンアップuseEffect発火後の後始末的なことをする
// コンポーネントが消滅する際に後始末(関数止めるとか)が必要な時に記述する。
// 基本書いておく方がいい
return () => {
window.clearInterval(intervalId);
};
}, []);
useEffect(() => {
console.log("up dated");
document.title = "counter:" + time;
window.localStorage.setItem("time-key-end", time);
return () => {
console.log("up dated end");
};
}, [time]);
return (
<h3>
<time>{time}</time>
<span>秒経過</span>
</h3>
);
};
export default Example;
useLayoutEffect
特性
useLayoutEffectはuseEffectよりも先に実行される。
先に実行したい処理はuseLayoutEffectの中に記述をする。
画面の更新より先にuseLayoutEffect内のコールバック関数が実行される。
再レンダリング時の画面のチラつきを抑えたい場合などの有効。
// useLayoutEffect: 同期的に実行(画面更新をブロック)
useLayoutEffect(() => {
// この処理が完了するまで画面が更新されない
}, []);
// useEffect: 非同期的に実行(画面更新をブロックしない)
useEffect(() => {
// 画面更新後に実行される
}, []);
// 悪い例:重い処理をuseLayoutEffectで行う
useLayoutEffect(() => {
// 重い計算や処理
// 画面更新がブロックされ、アプリケーションが遅く感じる
}, []);
// 良い例:重い処理はuseEffectで行う
useEffect(() => {
// 重い計算や処理
// 画面更新をブロックしないので、UXが向上
}, []);
import { useLayoutEffect, useEffect, useState, useRef } from "react";
const Random = () => {
const [state, setState] = useState(0);
// 画面の反映より先にuseLayoutEffect内のコールバック関数が実行される
useLayoutEffect(() => {
if (state === 0) {
setState(Math.random() * 300);
}
}, [state]);
return (
<button
className="effect-btn"
onClick={() => setState(0)}
style={{ fontSize: "1.8em" }}
>
state: {state}
</button>
);
};
export default Random;
使用例
import { useEffect, useState, useLayoutEffect } from "react";
const Example = () => {
// 表示か非表示かを管理するstate,初期値はtrue(表示)
const [isDisp, setIsDisp] = useState(true);
return (
<>
{/* 条件付きレンダリング、isDispの値がtrueならTimerコンポーネントを表示する */}
{isDisp && <Timer />}
<button onClick={() => setIsDisp((prev) => !prev)}>
{/* isDispの値がtrueなら非表示、falseなら表示の文字列を表示する */}
{isDisp ? "非表示" : "表示"}
</button>
</>
);
};
// Timerカウントコンポーネント
const Timer = () => {
// タイマーカウント更新のstate
const [time, setTime] = useState(0);
// タイマーが動いているかどうかを管理するstate
const [isRunning, setIsRunning] = useState(false);
// タイマーの制御とクリーンアップ処理、第二引数のisRunningの値が更新された時に実行される。
useEffect(() => {
let intervalId = null;
// isRunningがtrueの時だけタイマーを開始
if (isRunning) {
intervalId = window.setInterval(() => {
setTime((prev) => prev + 1);
}, 1000);
}
return () => {
window.clearInterval(intervalId);
};
}, [isRunning]);
// ローカルストレージに"time-key"という名前でtimeの値を保存する
useEffect(() => {
document.title = "counter:" + time;
window.localStorage.setItem("time-key", time);
return () => {};
}, [time]);
// 初回レンダリング時にローカルストレージから保存している値を読み込む、第二引数は空配列
useLayoutEffect(() => {
const _time = parseInt(window.localStorage.getItem("time-key"));
// データバリデーションのため必要、取得したtime-keyの値が数値ならsetTimeでstateを更新する
if (!isNaN(_time)) {
setTime(_time);
}
}, []);
// スタート・停止切り替える処理
const toggle = () => {
setIsRunning((prev) => !prev);
};
// カウントを0にリセットする処理
const reset = () => {
setTime(0);
setIsRunning(false);
};
return (
<>
<h3>
<time>{time}</time>
<span>秒経過</span>
</h3>
<button onClick={toggle}>{isRunning ? "一時停止" : "スタート"}</button>
<button onClick={reset}>リセット</button>
</>
);
};
export default Example;
カスタムフック
できること
- コードの再利用
- 同じロジックを使いまわせる(似たようなコンポーネントを作成する必要がない)
- ロジックとUIの分離
import { useEffect, useState, useLayoutEffect } from "react";
import useTimer from "./useTimer";
const Example = () => {
const [isDisp, setIsDisp] = useState(true);
return (
<>
{isDisp && <Timer />}
<button onClick={() => setIsDisp((prev) => !prev)}>
{isDisp ? "非表示" : "表示"}
</button>
</>
);
};
const Timer = () => {
// use⚪︎⚪︎で定義した関数をオリジナルフックとして使用することができる
const { time, isRunning, toggle, reset } = useTimer();
return (
<>
<h3>
<time>{time}</time>
<span>秒経過</span>
</h3>
<div>
<button onClick={toggle}>{isRunning ? "一時停止" : "スタート"}</button>
<button onClick={reset}>リセット</button>
</div>
</>
);
};
export default Example;
#useTimer.jsx
import { useLayoutEffect, useEffect, useState } from "react";
const useTimer = () => {
const [time, setTime] = useState(0);
const [isRunning, setIsRunning] = useState(false);
useEffect(() => {
// console.log('init');
let intervalId = null;
if (isRunning) {
// console.log('timer start');
intervalId = window.setInterval(() => {
// console.log('interval running');
setTime((prev) => prev + 1);
}, 1000);
}
return () => {
window.clearInterval(intervalId);
// console.log('end');
};
}, [isRunning]);
useEffect(() => {
// // console.log('updated');
document.title = "counter:" + time;
window.localStorage.setItem("time-key", time);
return () => {
// debugger
// // console.log('updated end');
};
}, [time]);
useLayoutEffect(() => {
const _time = parseInt(window.localStorage.getItem("time-key"));
if (!isNaN(_time)) {
setTime(_time);
}
}, []);
const toggle = () => {
setIsRunning((prev) => !prev);
};
const reset = () => {
setTime(0);
setIsRunning(false);
};
return {
time,
isRunning,
toggle,
reset,
};
};
export default useTimer;
関数型プログラミングの重要なキーワード
- 値の状態管理と処理を分離する
- 純粋関数(副作用を排除する)特定の入力には特定の出力を返す
- 不変性 一度設定した値は書き換えない(引数で渡ってくる値、オブジェクトのプロパティとか)
純粋関数
関数の戻り値が提供される入力値(引数)のみに依存する
関数の外外部スコープの状態(値)は参照・変更しない
引数で渡された値を変更しない
関数の外に影響を及ぼさない
これらを満たさない操作は副作用と呼ばれる
副作用とは
コンポーネントはJSXを構築する場所、JSXの構築に直接関係のない処理は全て**副作用(SideEffect)**として扱われる。
例)
- コンソールへのログの出力
- DOM通信
- サーバーとの通信
- タイマー処理 etc...
useEffect or イベントハンドラ内に記述する。コンポーネントの中には書かない!
Reactの関与しない処理は、useEffect内に書く。
Reduxとは
Reactと別の状態管理のためのライブラリ。React独自の技術じゃない!
使用する場合はRedux Toolkitを使用することが多い。
グローバルステートを管理する際にReduxを使用できる。
パフォーマンスの観点からグローバルステートを管理する際はReduxを使用して書いた方がいい。規模関わらず。
イミュータブル(不変性)とは
イミュータブルな値の変更とは
元の値、変数を上書きしない。
新しく配列やステートを作成して、変数の参照する先が変わっているため、元の値を上書きしたことにならない。ことを、イミュータブルな値の変更という。
ミュータブルな値の変更とは
変数の参照する先が変わらず、元の配列の中身が変わることをミュータブルな値の変更という
let val = [1,2,3,]
↓配列の変更
val.push(4);
イミュータビリティの保持
let val = [1,2,3]
↓配列をコピーして値を変更、スプレッド演算子などを使用
val = [...val,4]
Next.js
Next.jsの13.4からはルーティングの実装方法は基本的にAppRouter
既存プロジェクトの改修とかでいじる場合はPagesRouterも触ったほうがいいが今はスキップ
Next.jsとは?
React開発のフレームワーク。
Reactはライブラリ
Next.jsはフレームワーク
高速なWEBアプリケーションを作成するための機能が備わっている。
メリット
ゼロコンフィグ(手動設定不要)で高度な機能が使用できる
主な機能
- 複数のレンダリング方法(SSR,SG)
- ファイルベースルーティング・・・ファイル構成によってURLが生成される(ダイナミックルート)
- APIやアクションの作成(API Routes, Actions)
- ゼロコンフィグで開発者に優しい
Next.jsにおけるレンダリング
CSR - クライアントサイドレンダリング
- 特徴
- ルーティングの全てがクライアント上で行われる
- これまでのReact開発と同じ
- メリット
- 静的なファイルの配置のみで動く
- Node.jsの実行が必要ないので、サーバー負荷が軽い
- デメリット
- 初期描画(初期表示速度)に時間がかかる
- クローラーによってSEO的な問題あり(Google ChromeとかはJavaScriptの実行がされるから問題無いが、そうでないブラウザもある。。。)
SSR - サーバーサイドレンダリング
- 特徴
- Node.js(サーバー)にリクエストが来たタイミングで動的にHTMLを生成(生成されたHTML)
- 外部APIのデータの取得やコンポーネントのpropsの値を決定する処理を行い、HTMLを作成してクライアント側に返却する
- メリット
- 生成済みのHTMLを取得するのでSEOに強い
- デメリット
- 生成処理を全てサーバー側で行うため負荷大
- HTMLをクライアントに渡すまで時間がかかる
SG - 静的サイト生成
- 特徴
- ビルド時にデータフェッチやpropsの値の決定を行い、HTMLを構築する
- クライアントからリクエストされたら、サーバー側で構築することなく、生成済みのHTMLを渡す(あらかじめ作っておく)
- メリット
- 構築済みページのため表示速度が早い
- SEOも問題なし
- デメリット
- 更新頻度が高い動的コンテンツとの相性が悪い
Next.jsの基本構成
基本的なページはSGで生成
動的に作成する必要があるページはSSRを使用する。
近年はSSRを用いられる率が高い。
判断基準、分け方が気になる。
Next.jsにおけるファイル構成
app直下のpage.js
appディレクトリ配下にページの表示に必要なフォルダを配置する。
page.jsはリクエストが飛んできた時に表示するコンポーネントを記述する。
ルートページ(/)のコンテンツを定義する。
app直下のlayout.js
page.jsのコンポーネントをchildrenとして返す、全体の共通設定みたいなもの
ルートレイアウトで、アプリケーション全体の共通レイアウトを定義する。
app直下でページ名にしたいフォルダを作成し、その中にpage.jsを配置するとそのままそのファイル名がページリンク先になる。
例)
app - aboutフォルダ - page.jsに記述した内容
app/
├── page.js -> https://ドメイン/
├── about/
│ └── page.js -> https://ドメイン/about
├── products/
│ ├── page.js -> https://ドメイン/products
│ └── [id]/
│ └── page.js -> https://ドメイン/products/1 など
└── blog/
└── page.js -> https://ドメイン/blog
Next.jsの構成について
基本サーバーサイドで完結できるものはサーバー側で完結させる。
クライアント側で処理しなければならない処理は別のコンポーネントに分けて記述する。
#page.js
import "./lib.js";
import ClientComp from "./components/ClientComp.jsx";
export default function SSR() {
return (
<>
<div>SSR Page</div>
<ClientComp />
</>
);
}
#components - ClientComp.jsx
"use client";
import { useEffect, useState } from "react";
export default function SSR() {
const [state, setState] = useState(undefined);
// クライアントサイドで動かしたい処理はuseEffectで囲む
useEffect(() => {
setState("client loaded");
}, []);
return (
<>
<div>{state}</div>
</>
);
}
Next.jsにおけるデータフェッチのキャッシュの扱いについて
cache: "no-store"
動的データに使用
キャッシュを完全に無効化
毎回サーバーに新しいリクエストを送信
ユースケース:リアルタイムデータ、株価、ニュースフィードなど
デフォルト(cache: 'force-cache')
静的データに使用
レスポンスを永続的にキャッシュ
ビルド時にデータがフェッチされる
ユースケース:ブログ記事、製品情報など変更頻度が低いデータ
next: { revalidate: 秒数 }
定期的に更新が必要なデータに使用
指定した時間が経過後、バックグラウンドで再検証
ユースケース:定期的に更新される情報、天気予報など
export default async function SSR() {
// 1. キャッシュなし(毎回新しいデータをフェッチ)
const dynamicData = await fetch(ENDPOINT, {
cache: "no-store"
});
// 2. キャッシュあり(デフォルト)
const cachedData = await fetch(ENDPOINT);
// または明示的に
// cache: 'force-cache'
// 3. 時間指定での再検証(Incremental Static Regeneration)
const revalidatedData = await fetch(ENDPOINT, {
next: { revalidate: 10 } // 10秒ごとに再検証
});
}
例)
#page.js
import "./lib.js";
import ClientComp from "./components/ClientComp.jsx";
import { ENDPOINT } from "@/constants.js";
import ArticleList from "@/components/articleList/index.js";
export default async function SSR() {
// サーバーコンポーネント独自の書き方
// APIから情報取得して画面更新ができる
// fetch関数について
// 第二引数の中で、 { cache: "no-store" }を記述すると毎回キャッシュを使用せずに毎回リクエストの応答を使用するの意
// 記述しない場合は、cacheが有効になる
// { next: { revalidate: 10 } }を使用すると渡した秒数後にキャッシュを破棄することができる
const articles = await fetch(ENDPOINT, { next: { revalidate: 86400 } }).then(
(res) => res.json()
);
return (
<>
<div>SSR Page</div>
<ClientComp />
<ArticleList list={articles} basePath="/010_SSR" />
</>
);
}
ダイナミックルーティングでページを生成する
フォルダ名が意味を持つものがあるので注意、
階層構造やブログポストなど管理するときに普通のReactのルーティングで実装するよりもシンプルでわかりやすい。
app/
├── page.js # トップページ (/)
├── blog/
│ ├── page.js # ブログ一覧ページ (/blog)
│ ├── [id]/
│ │ ├── page.js # 個別記事ページ (/blog/1, /blog/2, etc.)
│ │ └── layout.js # 記事ページ共通レイアウト
│ └── categories/
│ ├── page.js # カテゴリー一覧 (/blog/categories)
│ └── [category]/
│ └── page.js # カテゴリー別記事一覧 (/blog/categories/tech)
└── layout.js # サイト共通レイアウト
// app/blog/page.js(一覧ページ)
export default async function BlogList() {
const posts = await fetch('API_ENDPOINT/posts').then(res => res.json());
return (
<div>
<h1>ブログ記事一覧</h1>
{posts.map(post => (
<Link key={post.id} href={`/blog/${post.id}`}>
<article>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
</Link>
))}
</div>
);
}
// app/blog/[id]/page.js(個別記事ページ)
export default async function BlogPost({ params }) {
const post = await fetch(`API_ENDPOINT/posts/${params.id}`).then(res => res.json());
return (
<article>
<h1>{post.title}</h1>
<div className="meta">
<time>{post.publishedAt}</time>
<span>{post.category}</span>
</div>
<div className="content">{post.content}</div>
</article>
);
}
// app/blog/[id]/layout.js(記事ページ共通レイアウト)
export default function BlogLayout({ children }) {
return (
<div className="blog-container">
<nav className="blog-nav">
<Link href="/blog">← 記事一覧に戻る</Link>
</nav>
{children}
<aside className="related-posts">
{/* 関連記事など */}
</aside>
</div>
);
}
SG静的ファイル生成として出力する方法
buildを実行するとoutディレクトリにhtmlファイルとして出力するための方法
・generateStaticParams()関数を定義する
import { paths } from "../paths";
export default function Page({ params }) {
const date = new Date();
return (
<h3>
このページは{params.id}です。{date.toJSON()}
</h3>
);
}
// 静的生成のために必須
export async function generateStaticParams() {
return paths;
}
・パスの定義(paths.js)
export const paths = [
{ id: '1' },
{ id: '2' },
{ id: '3' }
];
・#next.config.mjsdの設定を下記のようにする
import { paths } from "../paths";
export default function Page({ params }) {
const date = new Date();
return (
<h3>
このページは{params.id}です。{date.toJSON()}
</h3>
);
}
// SGとして定義する場合に必要な関数を定義する!!
export async function generateStaticParams() {
return paths;
}
#next.config.mjsd
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "export", // htmlとして出力する設定
trailingSlash: true, // url末尾にスラッシュをつけるかどうかの設定
};
export default nextConfig;
metadataの入れ方
参考サイト:https://nextjs.org/docs/app/api-reference/functions/generate-metadata
layout.js内にmetadataとしてオブジェクトを定義
// app/layout.js(ルート:サイト全体の設定)
export const metadata = {
title: {
template: '%s | サイト名',
default: 'サイト名',
}
};
// app/blog/[id]/page.js(個別ページ:特定のページの設定)
export async function generateMetadata({ params }) {
return {
title: '記事のタイトル' // 「記事のタイトル | サイト名」と表示
};
}
Next.jsがやってくれること:
このmetadataオブジェクトを自動的に読み取る
適切なHTMLのheadタグに変換
ページに自動で配置
具体例)
export const metadata = {
// 基本設定
title: {
default: "Next.js",
template: "%s | Next.js", // 子ページのタイトルテンプレート
},
description: "descriptionが入ります。",
// 言語設定
metadataBase: new URL('https://your-site.com'), // サイトのベースURL
// その他の重要なメタデータ
openGraph: {
title: 'Next.js',
description: 'descriptionが入ります。',
type: 'website',
locale: 'ja_JP',
siteName: 'サイト名',
},
// Twitter Card
twitter: {
card: 'summary_large_image',
title: 'Next.js',
description: 'descriptionが入ります。',
},
// その他の設定
viewport: {
width: 'device-width',
initialScale: 1,
},
robots: {
index: true,
follow: true,
},
};
export default function RootLayout({ children }) {
return (
<html lang="ja">
<head>
<link rel="icon" href="/favicon.ico" />
</head>
<body>{children}</body>
</html>
);
}
テスト
そもそもなぜテストを行う必要がある?
チーム開発の際など、テストがないとどこでエラーが出ているのか特定が難しくなる、そのためテストを実施して原因を切り分けられるようにする。
バグの早期発見が可能
- 単体テスト
- コンポーネント、関数単位のテスト
- 結合テスト
- アプリケーション全体のテスト
- 一気通貫テスト
- 外部システムなど含む全体のテスト
Reactでテストを行う際は、Jest,testing-libraryを使用してテストを行う。
Jext
複雑な設定が必要ないフレームワーク
- 初期設定不要
- テスト用関数(text.expect,toBe)などが利用可能
- 非同期処理のテスト
- 外部APIのmock化
TypeScriptについて
特徴
JavaScriptの拡張言語
- JSに変換してから実行される
- 型の定義が可能
- JSになり記述が使用可能
tscコマンドでJSに変換
メリット・デメリット
- メリット
- 型定義によるチーム開発の円滑化(1万大きい違い)
- 公開用ライブラリへの型定義
- バグの事前検知
- VSCodeの自動補完
- デメリット
- 型の記述が面倒
- JavaScript特有の柔軟で簡易な記述の喪失