こんにちは。
「JavaScript基礎習得メモ」の続きとして、本日から「React × TS × Tailwind × Next.js 入門メモ」のほうを始めていきます。
学習方法は、引き続きドットインストールの動画を見ていき、新たな知識や、実装しながら気づいたことを出来る限り多くメモしていきます。
React入門(全23回)
- Reactには、配列、
map()、filter()、分割代入、スプレッド構文の知識が要るので要復習! - Reactを動かすための設定:
ReactDOM.createRoot(container) - React内に,HTMLの直感的な書き方を内包したJSXが近年使われている
- HTMLの
babelの<script>(…babel/standalone/babel.min.js)と、 type属性(<script type="text/babel">)を書くことでJSXが使える
→ 本番環境ではNode.js等が必要なので、いずれ、そちらも学習予定
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My React App</title>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<!-- React本体のライブラリ。React.createElement()などが使えるようになる。-->
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<!-- DOMにReact要素を描画するためのライブラリ。ReactDOM.createRoot()などが使える。-->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<!-- JSX構文をブラウザ上で変換するためのBabelのミニ版。-->
<!-- これがないとJSXをそのまま書くことができない。-->
</head>
<body>
<div id="container"></div>
<script type="text/babel">
... // ここにJSXを書く
</script>
</body>
</html>
- 以下
<script>内の記述→ - 1.HTML上の
containerを取得し、ReactDOM.createRoot(container);で、rootに対して これから表示する内容をroot.render()内に指定
- 2.
root.render()内は普通のHTMLで書くことができるが、開始と終了の宣言としてフラグメントと呼ばれるタグ<></>で囲む
- 3.JSXでクラス名の付与をする時は
classNameを使う (N→大文字!!)
'use strict';
{
const container = document.querySelector('#container');
const root = ReactDOM.createRoot(container);
root.render(
<>
<h1>メニュー</h1>
<ul className="menus">
<li>牛丼</li>
<li>牛丼</li>
<li>牛丼</li>
</ul>
<p>合計: 0円</p>
</>
);
}
- 4.上記コードJSX部分をコンポーネントとして切り出すには:アロー関数で定義してその中に入れる(関数名の先頭は必ず大文字にする!)
root.render()の中は<app></app>と書くか、省略して<App />と書く
'use strict';
{
// コンポーネント(再利用可能部品)として切り出したコード
const App = () => { // 関数名の先頭は大文字
return (
<>
<h1>メニュー</h1>
<ul className="menus">
<li>牛丼</li>
<li>牛丼</li>
<li>牛丼</li>
</ul>
<p>合計: 0円</p>
</>
);
};
const container = document.querySelector('#container');
const root = ReactDOM.createRoot(container);
root.render(<App />);
}
- 上記コードから、さらにメニュー1つ1つもコンポーネントに切り出す
- 5.新たに定義した
Menu関数内のreturn内にメニューを移動させ、仮引数としてpropsをセットする。また、元々のJSXのメニュー部分にname属性で<Menu name="牛丼" />等として個々のメニューを追加する。
- 6.個々のメニューを画面に表示するために、
Menu関数内のメニューリスト内を{props.name}として波括弧で囲む
'use strict';
{
// コンポーネント(再利用可能部品)として新たに切り出したコード
const Menu = (props) => {
return (
<li>{props.name}</li>
);
};
// 個々のメニューを追加するコンポーネント
const App = () => {
return (
<>
<h1>メニュー</h1>
<ul className="menus">
<Menu name="牛丼" />
<Menu name="カレー" />
<Menu name="サラダ" />
</ul>
<p>合計: 0円</p>
</>
);
};
const container = document.querySelector('#container');
const root = ReactDOM.createRoot(container);
root.render(<App />);
}
- 7.
Menuコンポーネント内で<button>タグを使って+-ボタンを追加し、{props.name}を使用して価格の表示も追加する。また、Appコンポーネント内に個々のメニューの価格をprice属性で追加する - ※JSX(
return()内)のコメントアウトは{/* --- */}←波括弧を付ける
'use strict';
{
const Menu = (props) => {
return (
<li>
{/* 画面表示用に追加したボタンと価格表示 */}
<button>-</button>
<button>+</button>
{props.name} ({props.price}円 ☓ 0個)
</li>
);
};
const App = () => {
return (
<>
<h1>メニュー</h1>
<ul className="menus">
{/* メニューに加えて個々の価格も追加 */}
<Menu name="牛丼" price="400" />
<Menu name="カレー" price="500" />
<Menu name="サラダ" price="300" />
</ul>
<p>合計: 0円</p>
</>
);
};
const container = document.querySelector('#container');
const root = ReactDOM.createRoot(container);
root.render(<App />);
}
- 8.上記コードの
Appコンポーネント内のnamepriceを配列でまとめる
- 9.データの個数に応じて自動で
<Menu />を生成するため、mapを使う
復習☞map:配列に対して加工したり、新たに処理を加えるもの -
map内の自動化した配列の要素において、属性の値を埋め込むために{}を使う場合、""が不要で、=の直後に書かなくてはいけない(スペースが要らない)というルールがあるため、name={menu.name}と書く
※mapでの使用はあくまで一例で、正確には"関数コンポーネントに値(props)を渡す時に使用される書き方
- 注意!フラグメント
<></>の有無:下記コードでフラグメントが必要なのは、最後のreturn()のみ(付ける対象は、要素が複数かどうか)
➀Menuコンポーネント:<li>で1つの塊と捉えるため不要
➁menus.map()内:<Menu />を1つ返してるだけなので不要
➂Appコンポーネント:h1ulpと複数要素あるので必要
'use strict';
{
const Menu = (props) => {
return (
<li>
<button>-</button>
<button>+</button>
{props.name} ({props.price}円 ☓ 0個)
</li>
);
};
const App = () => {
// menusとして定義し配列化
const menus = [
{name: '牛丼', price: 400},
{name: 'カレー', price: 500},
{name: 'サラダ', price: 300},
];
// 配列をmapで自動化
const menuItems = menus.map((menu) => {
return (
<Menu
name={menu.name}
price={menu.price}
/>
);
});
return (// ここもAppコンポーネントなので注意
<>
<h1>メニュー</h1>
<ul className="menus">
{/* 関数化したのでこの辺りの記述を省略できる */}
{menuItems}
</ul>
<p>合計: 0円</p>
</>
);
};
const container = document.querySelector('#container');
const root = ReactDOM.createRoot(container);
root.render(<App />);
}
- 10.リストの項目には
keyという属性で'ユニークな値'(重複しない値)を付けるというルールがあるため、menusの配列内に0から連番でidを振り、menus.map内の属性の値にkey={menu.id}を追加する
- 11.ボタンをクリックすると個数が変化する機能を追加するため、
Menuコンポーネントのreturn外(最初)に、分割代入を使って、個数を管理する定数としてcount、そのcountを操作するための命令としてsetCountを同時に宣言し、値を保持する役目を果たすReact.useState(0);を書く。(初期値に0を入れておく)。個数には{count}個として埋め込む
React.useState()☞とてもよく使う!!ユーザーの操作によって変化させたい値があったら、useState() を使って、定数とそれを操作する命令を分割代入を使ってこのように宣言しておくという流れに慣れておく
'use strict';
{
const Menu = (props) => {
// ボタンをクリックすると個数が変化する機能を追加(値保持の機能)
const [count, setCount] = React.useState(0);
return (
<li>
<button>-</button>
<button>+</button>
{props.name} ({props.price}円 ☓ {count}個) {/* 個数管理のcount */}
</li>
);
};
const App = () => {
// menusとして定義し配列化
const menus = [
{id: 0, name: '牛丼', price: 400},
{id: 1, name: 'カレー', price: 500},
{id: 2, name: 'サラダ', price: 300},
];
// 配列をmapで自動化
const menuItems = menus.map((menu) => {
return (
<Menu
key={menu.id} // keyを追加
name={menu.name}
price={menu.price}
/>
);
});
return (// ここもAppコンポーネントなので注意
<>
<h1>メニュー</h1>
<ul className="menus">
{/* 関数化したのでこの辺りの記述を省略できる */}
{menuItems}
</ul>
<p>合計: 0円</p>
</>
);
};
const container = document.querySelector('#container');
const root = ReactDOM.createRoot(container);
root.render(<App />);
}
- 12.
-+ボタンにクリックイベントを設定する
普通のJavaScriptではaddEventListenerを使ったが、ReactではJSXの要素に直接onClick={}属性を使ってクリックイベントを追加する
- 13.また、
onClick={}へ渡すための関数としてdecrement(-ボタン)とincrement(+ボタン)を定義し、onClick={}へそれぞれ埋め込む
decrement関数:if文で'個数が0より多ければ'setCount(count - 1);
※個数カウントで-(マイナス)の値が表示されるのは不自然なため
increment関数:setCount(count + 1);
'use strict';
{
const Menu = (props) => {
// ボタンをクリックすると個数が変化する機能を追加(値保持の機能)
const [count, setCount] = React.useState(0);
// onClick={}へ渡すための個数増減関数
const decrement = () => {
if (count > 0) {
setCount(count - 1);
}
};
const increment = () => {
setCount(count + 1);
// setCount((prevCount) => { return prevCount + 1; });
// setCount((prevCount) => { return prevCount + 1; });
// prevCount(任意の変数追加)で、個数をまとめて増やす(上記の場合は3ずつ)
};
return (
<li>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
{props.name} ({props.price}円 ☓ {count}個) {/* 個数管理のcount */}
</li>
);
};
const App = () => {
// menusとして定義し配列化
const menus = [
{id: 0, name: '牛丼', price: 400},
{id: 1, name: 'カレー', price: 500},
{id: 2, name: 'サラダ', price: 300},
];
// 配列をmapで自動化
const menuItems = menus.map((menu) => {
return (
<Menu
key={menu.id} // keyを追加
name={menu.name}
price={menu.price}
/>
);
});
return (// ここもAppコンポーネントなので注意
<>
<h1>メニュー</h1>
<ul className="menus">
{/* 関数化したのでこの辺りの記述を省略できる */}
{menuItems}
</ul>
<p>合計: 0円</p>
</>
);
};
const container = document.querySelector('#container');
const root = ReactDOM.createRoot(container);
root.render(<App />);
}
- 14.
-+に連動して合計金額も増減させるため、ここでデータの管理方法を変更。Menuコンポーネントからconst [counts, setCounts] = React.useState(0);と、decrement関数、increment関数の中身を消し、const [count, setCount] = React.useState(0);はAppコンポーネントへ移動。初期値として[0, 0, 0]を入れる。(※[counts, setCounts]はs(複数形)を付けるのを忘れずに!!)
=親コンポーネント=Appがまとめて管理ことで、全メニューの個数が App に集約される!!
- 15.
menus.mapの属性にcountを追加してcount={counts[menu.id]}とする - 16.
MenuコンポーネントJSXの個数部分にもpropsを付ける
'use strict';
{
const Menu = (props) => {
const decrement = () => {};
const increment = () => {};
return (
<li>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
{props.name} ({props.price}円 ☓ {props.count}個)
{/* 個数管理のcount、ここにもpropsを付ける */}
</li>
);
};
const App = () => {
// Menuコンポーネントからの移動、初期値は[0,0,0]
// 3メニュー分まとめて親(Appコンポーネント)が持つ
const [counts, setCounts] = React.useState([0, 0, 0]);
// menusとして定義し配列化
const menus = [
{id: 0, name: '牛丼', price: 400},
{id: 1, name: 'カレー', price: 500},
{id: 2, name: 'サラダ', price: 300},
];
// 配列をmapで自動化
const menuItems = menus.map((menu) => {
return (
<Menu
key={menu.id} // keyを追加
count={counts[menu.id]} // countを追加
name={menu.name}
price={menu.price}
/>
);
});
return (// ここもAppコンポーネントなので注意
<>
<h1>メニュー</h1>
<ul className="menus">
{/* 関数化したのでこの辺りの記述を省略できる */}
{menuItems}
</ul>
<p>合計: 0円</p>
</>
);
};
const container = document.querySelector('#container');
const root = ReactDOM.createRoot(container);
root.render(<App />);
}
- 17.
Appコンポーネントに、合計金額を出す関数としてtotalを追加 - 18.
AppコンポーネントJSXの表示を{total}円にする - 19.
-ボタンを動作させる関数としてdecrementMenu追加 - 20.
menus.mapの属性にonDecrement={decrementMenu}追加 - 21.20で追加した
onDecrementをdecrement関数内で実行 - 22.
+ボタンを動作させる関数としてincrementMenu追加 - 23.
menus.mapの属性にonIncrement={incrementMenu}追加 - 24.23で追加した
onIncrementをincrement関数内で実行
'use strict';
{
const Menu = (props) => {
const decrement = () => {
props.onDecrement(); // -ボタンを動作させる関数の実行
};
const increment = () => {
props.onIncrement(); // +ボタンを動作させる関数の実行
};
return (
<li>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
{props.name} ({props.price}円 ☓ {props.count}個)
{/* 個数管理のcount、ここにもpropsを付ける */}
</li>
);
};
const App = () => {
// Menuコンポーネントからの移動、初期値は[0,0,0]
// 3メニュー分まとめて親(Appコンポーネント)が持つ
const [counts, setCounts] = React.useState([0, 0, 0]);
// menusとして定義し配列化
const menus = [
{id: 0, name: '牛丼', price: 400},
{id: 1, name: 'カレー', price: 500},
{id: 2, name: 'サラダ', price: 300},
];
// 合計金額を出す関数
// menusのpriceと対応するcountsの個数を掛け合わせたものを足す
const total =
(menus[0].price * counts[0]) +
(menus[1].price * counts[1]) +
(menus[2].price * counts[2]);
// -ボタンを動作させる関数
const decrementMenu = () => {
console.log('minus button clicked!');
};
// +ボタンを動作させる関数
const incrementMenu = () => {
console.log('plus button clicked!');
};
// 配列をmapで自動化
const menuItems = menus.map((menu) => {
return (
<Menu
key={menu.id} // keyを追加
count={counts[menu.id]} // countを追加
name={menu.name}
price={menu.price}
onDecrement={decrementMenu} // onDecrement追加
onIncrement={IncrementMenu} // onIncrement追加
/>
);
});
return (// ここもAppコンポーネントなので注意
<>
<h1>メニュー</h1>
<ul className="menus">
{/* 関数化したのでこの辺りの記述を省略できる */}
{menuItems}
</ul>
<p>合計: {total}円</p>
</>
);
};
const container = document.querySelector('#container');
const root = ReactDOM.createRoot(container);
root.render(<App />);
}
- ☆
decrementMenu関数、incrementMenu関数の処理を作る -
decrementMenu関数:現在のcountsの値をいったんコピーして、クリックされたメニューに対応する要素の値だけ1減らす処理を作る -
復習!スプレッド構文☞配列の中身(要素)を"展開"して、別の配列に入れる。例)moreScoresの[77, 88]をscoresの末尾に展開して入れる→
const scores = [70, 90, 80, 85, ...moreScores];
- 25.
newCounts関数を定義し、スプレッド構文で配列の要素を展開 - 26.
decrementMenu関数とnewCountsにmenuIdを渡し、--で減らす処理 - 27.
setCountsにnewCountsで更新 - 28.
menus.mapにmenuId={menu.id}を追加し、Menuコンポーネントのprops.onDecrementの実引数にprops.menuIdを渡す - 29.ここまでの処理で、
-ボタンを押すと個数、合計金額ともに-の値になってしまうため、decrementMenu関数にif文で条件分岐を追記 - 30.25~28の手順を汲んで、
incrementMenu関数も処理を追記
'use strict';
{
const Menu = (props) => {
const decrement = () => {
props.onDecrement(props.menuId); // -ボタンを動作させる関数の実行
// props.menuIdを渡す
};
const increment = () => {
props.onIncrement(props.menuId); // +ボタンを動作させる関数の実行
// props.menuIdを渡す
};
return (
<li>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
{props.name} ({props.price}円 ☓ {props.count}個)
{/* 個数管理のcount、ここにもpropsを付ける */}
</li>
);
};
const App = () => {
// Menuコンポーネントからの移動、初期値は[0,0,0]
// 3メニュー分まとめて親(Appコンポーネント)が持つ
const [counts, setCounts] = React.useState([0, 0, 0]);
// menusとして定義し配列化
const menus = [
{id: 0, name: '牛丼', price: 400},
{id: 1, name: 'カレー', price: 500},
{id: 2, name: 'サラダ', price: 300},
];
// 合計金額を出す関数
// menusのpriceと対応するcountsの個数を掛け合わせたものを足す
const total =
(menus[0].price * counts[0]) +
(menus[1].price * counts[1]) +
(menus[2].price * counts[2]);
// -ボタンを動作させる関数
const decrementMenu = (menuId) => { // ←ここのId付与忘れ注意!!
// 最初のメニューの-ボタンがクリックされた場合、
// [0, 0, 0] → [-1, 0, 0]として最初だけ-1する
// =現在のcountsの値をいったんコピーして、
// クリックされたメニューに対応する要素の値だけ1減らす処理を作る
// 配列の角括弧の中で、スプレッド構文を使って
// countsの要素を展開すれば、コピーを作ることができる
// そして、newCountsのうち、
// クリックされたメニューに対応する要素としてmenuIdを追加
// 加えて、個数、合計金額が-の値にならないようif文を追記、
// ここまでに書いた3つの処理を包む
if (counts[menuId] > 0) {
const newCounts = [...counts];
newCounts[menuId]--; // --で'menuId番目の要素の値を1減らす'
setCounts(newCounts); // countsを更新されたnewCountsで更新
}
};
// +ボタンを動作させる関数(おおよそは-の方を参照)
const incrementMenu = (menuId) => { // ←ここのId付与忘れ注意!!
const newCounts = [...counts];
newCounts[menuId]++;
setCounts(newCounts);
};
// 配列をmapで自動化
const menuItems = menus.map((menu) => {
return (
<Menu
key={menu.id} // keyを追加
menuId={menu.id} // menuIdを追加
count={counts[menu.id]} // countを追加
name={menu.name}
price={menu.price}
onDecrement={decrementMenu} // onDecrement追加
onIncrement={incrementMenu} // onIncrement追加
// 大文字、小文字のタイプミスに注意!!
/>
);
});
return (// ここもAppコンポーネントなので注意
<>
<h1>メニュー</h1>
<ul className="menus">
{/* 関数化したのでこの辺りの記述を省略できる */}
{menuItems}
</ul>
<p>合計: {total}円</p>
</>
);
};
const container = document.querySelector('#container');
const root = ReactDOM.createRoot(container);
root.render(<App />);
}
ReactでTodo管理アプリを作ろう(全20回)
-
Appコンポーネント作成 - 入門編でも見た通り、JSXは複数の値を渡すことは出来ないため、代わりにフラグメント
<></>で囲む -
<input type="checkbox" />のように、inputタグの最後に/追加 - お馴染みの
{}はJSXパート用の埋め込みなので書き忘れ注意
'use strict';
{
const App = () => {
return (
<>
{/* JSX内にて、フラグメントで囲ってから
HTML要素を入れる */}
<h1>
Todos
<button id="purge">Purge</button>
</h1>
<ul id="todos">
<li>
<label>
<input type="checkbox" />
<span>
aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa
</span>
</label>
<button>Del</button>
</li>
<li>
<label>
<input type="checkbox" />
<span>
aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa
</span>
</label>
<button>Del</button>
</li>
</ul>
<form id="add-form">
<input type="text" />
<button>Add</button>
</form>
</>
);
};
const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(
<App />
);
}
-
<li>の中身をTodoコンポーネントに分ける - 表示したいリストは、オブジェクトの配列で管理
-
.map()で1個ずつ部品に変えて表示 - タイトルは
props.todo.titleで受け取って表示
'use strict';
{
// 【変更点①】元々Appの中に直接<li>で書いていた「1件分の表示」を
// Todoという名前の小さな部品(コンポーネント)として切り出した
const Todo = (props) => {
return (
<li>
<label>
<input type="checkbox" />
{/* 元々は "aaa aaa aaa..." とベタ書きしていた部分
→ 今はprops(親から渡されたデータ)を表示するように変更 */}
<span>{props.todo.title}</span>
</label>
<button>Del</button>
</li>
);
};
// アプリ全体をまとめる部品(コンポーネント)
const App = () => {
// 【変更点②】リストの中身を「配列」でまとめて管理するようにした
// 1つ1つの項目に「id(番号)」「title(文字)」「isCompleted(完了したか)」を持たせている
const todos = [
{id: 0, title: 'aaa', isCompleted: false},
{id: 1, title: 'bbb', isCompleted: true},
{id: 2, title: 'ccc', isCompleted: false},
];
// 【変更点③】配列から1件ずつ取り出して、Todo部品に変換する
// 「key」は表示の順番がズレないようにするための番号(Reactが管理用に使う)
const todoItems = todos.map((todo) => {
return (
<Todo
key={todo.id} // 各項目に番号をつけて「どれがどれか」を区別できるようにしている
todo={todo} // 項目の中身(titleなど)をTodo部品に渡す
/>
);
});
return (
<>
<h1>
Todos
<button id="purge">Purge</button>
</h1>
{/* 【変更点④】元々は<li>を直書きしていた場所
→ Todoという部品をループで並べて表示するようにした */}
<ul id="todos">
{todoItems}
</ul>
{/* 入力フォーム(追加ボタンなどの機能はまだ作っていない) */}
<form id="add-form">
<input type="text" />
<button>Add</button>
</form>
</>
);
};
// HTML側の<div id="root">にApp部品を表示する
const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(
<App />
);
}
- Todo削除機能を追加
- 元々はボタンを押しても何も起きなかった → 今は削除できるように
-
App側で削除の処理を用意 →Todo側から実行できるように関数を渡す - 配列の操作は
filter()を使って削除以外を残す方式に変更
'use strict';
{
// 【変更点①】
// 元々はTodo部品に「削除するためのクリックイベント」がなかった
// → Delボタンがクリックされたときに、親のAppに削除を伝えるように変更
const Todo = (props) => {
// 【追加】関数 handleDeleteClickを定義
// → この関数が呼ばれると、親から渡されたonDeleteClickを実行する
//(どのTodoを消したいか分かるようにidも一緒に渡している)
const handleDeleteClick = () => {
props.onDeleteClick(props.todo.id);
};
return (
<li>
<label>
<input type="checkbox" />
<span>{props.todo.title}</span>
</label>
{/* 元々は <button>Del</button> だけだったが、
今は onClick で handleDeleteClick を実行するよう変更 */}
<button onClick={handleDeleteClick}>Del</button>
</li>
);
};
const App = () => {
// 【変更点②】
// 元々は const todos = [...] のように固定データだったが、
// → 今は useState で todos を「状態」として管理できるようにした
// これにより setTodos で中身を書き換えれば画面が自動で更新される
// 定数とその定数を操作するための命令が配列で返ってくる
const [todos, setTodos] = React.useState([
{id: 0, title: 'aaa', isCompleted: false},
{id: 1, title: 'bbb', isCompleted: true},
{id: 2, title: 'ccc', isCompleted: false},
]);
// 【変更点③】
// 削除機能追加に伴う変更
// → idを受け取って、そのidに一致しないデータだけを残した新しい配列を作成
// 理由:配列を直接いじると不具合が出るため、
//「削除されたものを除いた新しい配列」を作り直す
const handleTodoDeleteClick = (id) => {
if (!confirm('Sure?')) {
return;
}
// 【重要】filterで、todo.id が削除対象と「一致しない」ものだけを残す
// → 実質的に「このidのtodoだけ消す」ことになる
const newTodos = todos.filter((todo) => {
return todo.id !== id;
});
// 新しい配列を setTodos で上書きし、Reactに再描画させる
setTodos(newTodos);
};
// 【変更点④】
// 削除用の関数 onDeleteClick を追加
const todoItems = todos.map((todo) => {
return (
<Todo
key={todo.id} // 表示順がズレないように、各Todoに番号を渡す
todo={todo} // 中身(titleなど)を渡す
onDeleteClick={handleTodoDeleteClick}
// 削除用の関数を渡す(子から親へ通知するため)
/>
);
});
return (
<>
<h1>
Todos
<button id="purge">Purge</button>
</h1>
<ul id="todos">
{todoItems}
</ul>
<form id="add-form">
<input type="text" />
<button>Add</button>
</form>
</>
);
};
const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(
<App />
);
}
-
Todo部品にチェックON/OFFを受け取る仕組みを追加 - 親から
propsでisCompletedを渡してcheckedに反映 - 変更イベントが起きたら、親の
Appに「このTodoのチェックを切り替えて」と伝える
'use strict';
{
// 【変更点①】
// 元々Todo部品ではチェックボックスを押しても何も起きなかった
// → チェック状態を切り替えるイベントを追加(親に変更を伝える)
const Todo = (props) => {
// 【既存】削除ボタンが押されたときの処理
const handleDeleteClick = () => {
props.onDeleteClick(props.todo.id);
};
// 【追加】チェックボックスが押されたときの処理
// → チェックのON/OFFを親に伝える(どのtodoか分かるようにidを渡す)
const handleCheckboxChange = () => {
props.onCheckboxChange(props.todo.id);
};
return (
<li>
<label>
{/* 【変更点②】
元々は <input type="checkbox" /> だけだった
→ チェック状態をpropsから受け取って反映(checked属性)
→ onChangeイベントでhandleCheckboxChangeを実行 */}
<input
type="checkbox"
checked={props.todo.isCompleted}
// チェックされてるかどうか(true/false)を反映
onChange={handleCheckboxChange} // クリックされたときの処理
/>
<span>{props.todo.title}</span>
</label>
<button onClick={handleDeleteClick}>Del</button>
</li>
);
};
const App = () => {
// 【既存】状態(配列)としてtodoリストを保持
const [todos, setTodos] = React.useState([
{id: 0, title: 'aaa', isCompleted: false},
{id: 1, title: 'bbb', isCompleted: true},
{id: 2, title: 'ccc', isCompleted: false},
]);
// 【変更点③】
// 元々はチェックボックスの切り替えに対応していなかった
// → チェック状態(true/false)を反転させて再セットする処理を追加
const handleTodoCheckboxChange = (id) => {
// 【重要】mapで全要素をループし、対象のidだけisCompletedを反転
// 理由:Reactでは状態を直接変更せず「新しい配列を作って渡す」のが基本ルール
const newTodos = todos.map((todo) => {
// 該当するTodoだけisCompletedを反転した新配列を作る処理
return {
id: todo.id, // idはそのまま
title: todo.title, // titleもそのまま
isCompleted: todo.id === id
// 対象のidと一致すれば反転(true→false / false→true)
? !todo.isCompleted
: todo.isCompleted, // 一致しないものは元のまま
};
});
setTodos(newTodos); // 更新して再描画
};
// 【既存】削除処理(変更なし)
const handleTodoDeleteClick = (id) => {
if (!confirm('Sure?')) {
return;
}
const newTodos = todos.filter((todo) => {
return todo.id !== id;
});
setTodos(newTodos);
};
// 【変更点④】
// 元々は <Todo ... onDeleteClick={...} /> だけだった
// → onCheckboxChange も追加で渡して、子コンポーネントで使えるようにした
const todoItems = todos.map((todo) => {
return (
<Todo
key={todo.id}
todo={todo}
onDeleteClick={handleTodoDeleteClick}
onCheckboxChange={handleTodoCheckboxChange} // 追加
/>
);
});
return (
<>
<h1>
Todos
<button id="purge">Purge</button>
</h1>
<ul id="todos">
{todoItems}
</ul>
<form id="add-form">
<input type="text" />
<button>Add</button>
</form>
</>
);
};
const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(
<App />
);
}
-
AddFormという部品(コンポーネント)を新しく追加 - フォームの入力値は
titleという状態(state)で管理 - フォームが送信されたら
Appに通知しtodosに新しい項目を追加 -
valueとonChangeでフォーム入力をリアルタイム管理
(=制御コンポーネント)
-
AddForm(子コンポーネント)- フォームが送信されると... ←(ユーザー操作)
-
handleSubmit()が呼ばれる ←(子の中の関数) -
props.onSubmit(title)を実行 ←(親にデータを渡す)
-
App(親コンポーネント)-
onSubmit={handleAddFormSubmit}←(親が関数を渡していた) -
handleAddFormSubmit(title)実行 ←(ここで実際にTodo追加処理)
-
- ユーザー側の処理
- 入力欄に文字を打つ →
handleTextChange()でtitleを更新 - ボタンを押す(=送信) →
handleSubmit()が呼ばれる -
props.onSubmit(title)→ 親に今の入力内容を通知 -
setTitle('')→ 入力欄を空に戻す
- 入力欄に文字を打つ →
'use strict';
{
// Todo項目1つ分の見た目と動きを定義した部品
// チェック状態や削除ボタンなどを含む
const Todo = (props) => {
// 削除ボタンを押した時の動作(親から渡された関数を実行)
const handleDeleteClick = () => {
props.onDeleteClick(props.todo.id);
};
// チェックボックスを切り替えた時の動作(親へ通知)
const handleCheckboxChange = () => {
props.onCheckboxChange(props.todo.id);
};
return (
<li>
<label>
<input
type="checkbox"
checked={props.todo.isCompleted} // 完了していたらチェックON
onChange={handleCheckboxChange} // チェック切替で状態更新
/>
<span>{props.todo.title}</span> {/* タイトル文字列の表示 */}
</label>
<button onClick={handleDeleteClick}>Del</button>
</li>
);
};
// フォーム入力部品:新しいTodoを追加するためのフォーム
const AddForm = (props) => {
// 【変更点①】
// 入力欄の内容を保存する状態を追加
// 元々は<input>がAppに直書きされていた
// → 今はAddFormという部品に分け、状態で文字列をリアルタイムに管理
const [title, setTitle] = React.useState('');
// 入力欄に文字が入力されるたびに状態を更新する処理
const handleTextChange = (e) => {
setTitle(e.currentTarget.value);
};
// フォームが送信されたときの処理
const handleSubmit = (e) => {
e.preventDefault(); // 画面の自動リロードを防止
props.onSubmit(title); // 入力された文字列を親コンポーネントへ渡す
setTitle(''); // 入力欄をリセット(初期化)
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={title} // 状態に応じて入力欄を更新
onChange={handleTextChange}
/>
<button>Add</button>
</form>
);
};
// アプリ全体のコンポーネント(状態とロジックをまとめて管理)
const App = () => {
// Todoリストを状態で管理(id, タイトル, 完了済かどうか)
const [todos, setTodos] = React.useState([
{id: 0, title: 'aaa', isCompleted: false},
{id: 1, title: 'bbb', isCompleted: true},
{id: 2, title: 'ccc', isCompleted: false},
]);
// 【変更点②】
// AddFormから渡されたタイトル文字列をもとに新しいTodoを作って追加
// 親(App)側で新しいTodoを作って配列に追加する関数
const handleAddFormSubmit = (title) => {
const newTodos = [...todos]; // 現在の配列をコピー
newTodos.push({
id: Date.now(), // 一意のidを自動で生成(現在時刻ミリ秒)
title: title, // 入力された文字列をタイトルに設定
isCompleted: false, // 追加直後は未完了にする
});
setTodos(newTodos); // 状態を更新(=再描画)
};
// チェック状態を切り替えるときの処理
const handleTodoCheckboxChange = (id) => {
// idが一致するTodoだけisCompletedを反転、それ以外はそのまま
const newTodos = todos.map((todo) => {
return {
id: todo.id,
title: todo.title,
isCompleted: todo.id === id ? !todo.isCompleted : todo.isCompleted,
};
});
setTodos(newTodos);
};
// 削除ボタンが押されたときの処理
const handleTodoDeleteClick = (id) => {
if (!confirm('Sure?')) {
return;
}
// 指定されたidと一致しないものだけを残す
const newTodos = todos.filter((todo) => {
return todo.id !== id;
});
setTodos(newTodos); // 状態を更新(削除を反映)
};
// Todo配列をもとに<Todo />部品を複数生成
const todoItems = todos.map((todo) => {
return (
<Todo
key={todo.id} // Reactが再描画のために使う識別子
todo={todo} // 1件分のTodoデータ
onDeleteClick={handleTodoDeleteClick}
onCheckboxChange={handleTodoCheckboxChange}
/>
);
});
return (
<>
<h1>
Todos
<button id="purge">Purge</button>
</h1>
<ul id="todos">
{todoItems}
</ul>
{/* 【変更点③】元々は<form>が直書きされていたが、AddFormとして独立 */}
<AddForm
onSubmit={handleAddFormSubmit}
// 子から親へ「追加して~」と伝えるための props
/>
</>
);
};
const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(
<App />
);
}
-
useRefを使って入力欄にフォーカス(AddForm内) -
Purgeボタンで「完了済みのTodoだけまとめて削除」(App内) -
useRefって何?:イメージと仕組み-
const inputRef = React.useRef(null);:「HTMLの<input>要素を直接つかむための変数を作る」React特有の書き方 - ☆使い方3ステップ
- 参照用の変数を作る →
useRef(null)で準備 - JSXで参照対象に指定する →
<input ref={inputRef} /> - そのDOMに対して
.focus()などを実行 - →
inputRef.current.focus()←これが実際のDOM操作!
-
'use strict';
{
// Todo項目1つ分の見た目と動きを定義した部品
// チェック状態や削除ボタンなどを含む
const Todo = (props) => {
// 削除ボタンを押した時の動作(親から渡された関数を実行)
const handleDeleteClick = () => {
props.onDeleteClick(props.todo.id);
};
// チェックボックスを切り替えた時の動作(親へ通知)
const handleCheckboxChange = () => {
props.onCheckboxChange(props.todo.id);
};
return (
<li>
<label>
<input
type="checkbox"
checked={props.todo.isCompleted} // 完了していたらチェックON
onChange={handleCheckboxChange} // チェック切替で状態更新
/>
<span>{props.todo.title}</span> {/* タイトル文字列の表示 */}
</label>
<button onClick={handleDeleteClick}>Del</button> {/* 削除ボタン */}
</li>
);
};
// フォーム入力部品:新しいTodoを追加するためのフォーム
const AddForm = (props) => {
// 入力欄の状態管理(文字の内容)
const [title, setTitle] = React.useState('');
// 初期値を ''(空文字)にすることで、フォームが空の状態から始まる
// → 入力後、setTitle('')で初期化することで連続入力を可能に
// 【変更点①】入力欄に自動でフォーカスするための参照を作成
const inputRef = React.useRef(null);
// useRefは「あるDOM要素を直接操作したい時」に使うReactの仕組み
// ここでは <input> 要素を参照するために使っている
// 初期値nullとは「まだ何も参照していない」ことを意味する
// → renderされたあとにinputRef.currentにDOMが代入される
// 入力欄に文字が入力されるたびに状態を更新する処理
const handleTextChange = (e) => {
setTitle(e.currentTarget.value); // 入力中の文字列をstateへ反映
};
// フォームが送信されたときの処理
const handleSubmit = (e) => {
e.preventDefault(); // 画面の自動リロードを防止
props.onSubmit(title); // 入力された文字列を親コンポーネントへ渡す
setTitle(''); // 入力欄をリセット(初期化)
inputRef.current.focus(); //【変更点②】次の入力に備えてフォーカスを戻す
// useRefを使うと、DOMに直接アクセスしてfocus()を呼べる
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={title} // 状態に応じて入力欄を更新
onChange={handleTextChange} // 入力時の変更イベント
ref={inputRef} // 【変更点③】この<input>を参照対象に設定
/>
<button>Add</button> {/* 追加用ボタン(Enterでも反応) */}
</form>
);
};
// アプリ全体の管理を行うコンポーネント
const App = () => {
// Todoリストの状態を管理(配列)
const [todos, setTodos] = React.useState([
{id: 0, title: 'aaa', isCompleted: false},
{id: 1, title: 'bbb', isCompleted: true},
{id: 2, title: 'ccc', isCompleted: false},
]);
// 【変更点④】完了済みのTodoを一括削除するボタンの処理
// 元々は個別削除しかできなかったが、
// チェック済みだけをまとめて削除したくなったため追加
const handlePurgeClick = () => {
if (!confirm('Sure?')) {
return; // ユーザーがキャンセルした場合は処理しない
}
// チェックされていないTodo(isCompleted: false)だけを残す
const newTodos = todos.filter((todo) => {
return todo.isCompleted === false;
// 完了した(true)ものは削除したいのでfalseのものだけ残す
});
setTodos(newTodos); // 配列を更新
};
// 新しいTodoを追加する処理(AddFormから受け取る)
const handleAddFormSubmit = (title) => {
const newTodos = [...todos]; // 既存の配列をコピー(スプレッド構文)
newTodos.push({
id: Date.now(), // 一意なIDを生成(ミリ秒単位の現在時刻)
title: title,
isCompleted: false, // 初期状態は未完了
});
setTodos(newTodos); // 配列を更新
};
// チェック状態を切り替える処理(ON/OFF)
const handleTodoCheckboxChange = (id) => {
const newTodos = todos.map((todo) => {
return {
id: todo.id,
title: todo.title,
isCompleted: todo.id === id ? !todo.isCompleted : todo.isCompleted,
// 指定したidと一致した要素だけ完了状態を反転させる
};
});
setTodos(newTodos);
};
// 個別の削除ボタンが押された時の処理
const handleTodoDeleteClick = (id) => {
if (!confirm('Sure?')) {
return;
}
// 指定したid以外のTodoだけを残す(そのidの要素を除く)
const newTodos = todos.filter((todo) => {
return todo.id !== id;
});
setTodos(newTodos);
};
// 表示用のJSXを作成(Todoリスト)
const todoItems = todos.map((todo) => {
return (
<Todo
key={todo.id} // 各項目に一意のkeyを設定
todo={todo} // 個別のTodoオブジェクトを渡す
onDeleteClick={handleTodoDeleteClick}
onCheckboxChange={handleTodoCheckboxChange}
/>
);
});
return (
<>
<h1>
Todos
{/* 【変更点⑤】一括削除ボタンを表示(完了済みだけ消える) */}
<button onClick={handlePurgeClick}>Purge</button>
</h1>
<ul id="todos">
{todoItems}
</ul>
{/* 入力フォーム(AddFormコンポーネントを使用) */}
<AddForm onSubmit={handleAddFormSubmit} />
</>
);
};
const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);
}
-
useRefを導入し、フォーム送信後に入力欄へ自動でフォーカスを戻す機能を追加 -
todosの初期値を空配列に(元々は固定の初期タスク3件) -
useEffectを用いて、初回レンダリング時にlocalStorageから保存済タスクを取得 -
updateTodos()を新規関数として定義し、setTodos()+localStorage.setItem()を一括処理する共通関数に集約
'use strict';
{
// Todo1つ分の表示・削除・完了切替を扱う部品
const Todo = (props) => {
// 削除ボタンを押した時の処理
const handleDeleteClick = () => {
props.onDeleteClick(props.todo.id);
};
// チェックボックス(完了フラグ)を切り替えた時の処理
const handleCheckboxChange = () => {
props.onCheckboxChange(props.todo.id);
};
return (
<li>
<label>
<input
type="checkbox"
checked={props.todo.isCompleted} // 完了済みかどうか(状態)を反映
onChange={handleCheckboxChange}
// 変更イベントで状態更新処理を親へ伝える
/>
<span>{props.todo.title}</span> {/* タスクのタイトルを表示 */}
</label>
<button onClick={handleDeleteClick}>Del</button> {/* 削除ボタン */}
</li>
);
};
// 入力フォーム(タスク追加用)
const AddForm = (props) => {
const [title, setTitle] = React.useState(''); // 入力欄のテキスト状態
// 入力欄への自動フォーカス用に参照を用意
// 元々は何もしていなかった → 連続入力をスムーズにするために追加
const inputRef = React.useRef(null); // useRefでinput要素にアクセス
// 入力欄に文字が入力されるたびに状態を更新
const handleTextChange = (e) => {
setTitle(e.currentTarget.value); // 入力値を反映
};
// フォーム送信時の処理(ボタン押す or Enter)
const handleSubmit = (e) => {
e.preventDefault(); // ブラウザのデフォ動作(リロード)を抑止
props.onSubmit(title); // Appにデータを渡す
setTitle(''); // 入力欄をクリア
inputRef.current.focus(); // 送信後に再フォーカスを戻す
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={title}
onChange={handleTextChange}
ref={inputRef}
/>
<button>Add</button>
</form>
);
};
// アプリ全体を制御するメインコンポーネント
const App = () => {
// 【変更点①】元々はダミーデータ3件が初期値だった → 空配列に変更
// localStorageから読み込む仕様に変わったため
const [todos, setTodos] = React.useState([]);
// 【変更点②】初回マウント時にlocalStorageからデータ復元
// マウント=Reactコンポーネントが初めて画面に表示(DOMに追加)されるタイミング
React.useEffect(() => {
// マウント時1回だけ実行されるReactの仕組み。初期読み込みや初期化に使う
let savedTodos;
if (localStorage.getItem('todos') === null) {
savedTodos = []; // データがない場合は空配列
} else {
// JSON.parseで保存文字列をオブジェクトに戻す
savedTodos = JSON.parse(localStorage.getItem('todos'));
}
setTodos(savedTodos); // 状態に反映(再描画)
}, []); // 空配列 → マウント時1回だけ実行
// 【変更点③】状態更新+保存をまとめて行う共通関数
const updateTodos = (newTodos) => {
setTodos(newTodos); // 状態を更新
localStorage.setItem('todos', JSON.stringify(newTodos));
// localStorageは文字列しか保存できないため
// オブジェクトを保存するためJSONを文字列化
};
// 完了済みタスクを一括削除
const handlePurgeClick = () => {
if (!confirm('Sure?')) return;
const newTodos = todos.filter((todo) => !todo.isCompleted);
updateTodos(newTodos); // 【変更点③-1】setTodos → updateTodos に統一
};
// タスク追加処理(AddFormから受け取る)
const handleAddFormSubmit = (title) => {
const newTodos = [...todos]; // 既存配列をコピー
newTodos.push({
id: Date.now(), // ユニークなID生成(現在時刻)
title: title,
isCompleted: false, // 初期状態:未完了
});
updateTodos(newTodos); // 【変更点③-2】
};
// チェックボックス切り替え処理
const handleTodoCheckboxChange = (id) => {
const newTodos = todos.map((todo) => {
return {
id: todo.id,
title: todo.title,
isCompleted: todo.id === id ? !todo.isCompleted : todo.isCompleted,
// 対象のタスクだけ完了状態を反転
};
});
updateTodos(newTodos); // 【変更点③-3】
};
// 個別削除処理(Delボタン)
const handleTodoDeleteClick = (id) => {
if (!confirm('Sure?')) return;
const newTodos = todos.filter((todo) => todo.id !== id);
updateTodos(newTodos); // 【変更点③-4】
};
// Todo一覧のJSX配列を生成(map)
const todoItems = todos.map((todo) => {
return (
<Todo
key={todo.id}
todo={todo}
onDeleteClick={handleTodoDeleteClick}
onCheckboxChange={handleTodoCheckboxChange}
/>
);
});
return (
<>
<h1>
Todos
<button onClick={handlePurgeClick}>Purge</button>
{/* 完了済み削除 */}
</h1>
<ul id="todos">
{todoItems}
</ul>
<AddForm onSubmit={handleAddFormSubmit} />
</>
);
};
const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);
}
TypeScript入門(全16回)
- JSには、例えば
letで数値のあとに文字列を代入しても問題ない仕様になっているが、他の人と開発をするようになると、同じ種類の値しか代入できない、というルールにした方が、変数の挙動が分かりやすくなって、結果としてより安全なプログラムを書けるようになる - → TypeScriptでは、変数を宣言するときにそこに代入できる値の種類も合わせて指定することで、うっかり他の種類の値が代入されたときには、エラーではじいてくれるという仕組みになっている
- ファイル拡張子は
.ts(TypeScriptの略) - 数値の型付け:
let x: number(コロン:の後にnumber)
'use strict';
{
let x: number = 5;
x = 'five'; // 文字列を代入するとエラーではじかれる
console.log(x);
}
- 文字列の型付け:
let x: string(コロン:の後にstring)
'use strict';
{
let message: string;
message = 'Hello';
message = 50; // 数値を代入するとエラーではじかれる
console.log(message.length); // 使える
console.log(message.toFixed(2)); // 数値にしか使えない命令もはじかれる
}
- 真偽値の型付け:
let x: boolean(コロン:の後にboolean) - 型推論:
trueを代入した最初のコードでts側が真偽値の型付け判定をしてくれるため、: booleanは省略できる - 型推論をしてくれる箇所であえて型付けを書くかどうかは、一緒に働いているチームのルールに従ったり、自身にとって分かりやすい方を使う
'use strict';
{
let isLoggedIn: boolean = true;
// 型推論で let isLoggedIn = true; と書くのもOK
let x: undefined = undefined;
let y: null = null;
}
- ユニオン型:
|で繋ぐことで複数の型を代入できる
'use strict';
{
let keyword: string | number | boolean;
keyword = 'milk'; // OK!
keyword = 50; // OK!
keyword = true; // OK!
}
- リテラル型:値そのものが型になっている書き方。
PassかFailのどちらかしか入れさせない!というような複数の“限定された値”だけを許可したい時、リテラル型とユニオン型がセットで使われる。リテラル型があることにより、大文字小文字や入力値などのミス防止に繋げられる
'use strict';
{
let taxRate: 0.1;
let myEmail: 'taro@example.com';
let isPaid: true;
myEmail = 'jiro@example.com'; // エラー
let result: 'Pass' | 'Fail';
result = 'Good'; // エラー
result = 50; // エラー
}
- 型に名前を付けておくと保守性が高まる。
typeキーワードを使って型に名前をつける場合、最初の1文字目は大文字にする -
type~ではなくinterface~でも可(詳細は'オブジェクトの型付け'の項)
'use strict';
{
type ResultStatus = 'Pass' | 'Fail';
// 最初の1文字目は大文字
let englishResult: ResultStatus;
let mathResult: ResultStatus;
}
- 配列の型付け:配列の値を
number[]のように[]を付けて指定 - 配列は
constで宣言していても要素の中身を書き換えることができるが、型の前にreadonlyキーワードを付ければ書き替え不能になる
'use strict';
{
const scores: readonly number[] = [70, 90, 80];
// 配列の型を数値として指定(+読み取り専用)
scores[1] = 100; // 読み取り専用のためエラー
scores.push(60); // 読み取り専用のためエラー
scores[2] = 'Hello'; // 文字列を入れる+読み取り専用→エラー
scores.push('OK'); // 文字列を要素追加+読み取り専用→エラー
const values: (string | number)[] = ['Taro', 70, 'Jiro'];
// 文字列や数値でのみ配列要素の編集を許可
values[2] = 80; // OK!
values.push('Saburo'); // OK!
values.push(true); // 真偽値を入れる→エラー
}
- タプル:配列の要素を順序とともに指定するデータ型
'use strict';
{
const values: [string, number] = ['Taro', 70];
// 0番目は必ず文字列、1番目は必ず数値、それ以外はエラー
// これもreadonlyが使える。const values: readonly [string, number]...
values[0] = 'Jiro'; // OK!
values[1] = 90; // OK!
values[0] = 70; // エラー
values[1] = 'Saburo'; // エラー
}
- オブジェクトの型付け:オブジェクトにそれぞれ特定の型を指定することで、存在しないプロパティを設定しようとするとエラーではじく
➀interface Userで型に名前を付ける(interface~は近年 主流の書き方)
➁const user: User←User部分={userName: string; score: number}
'use strict';
{
interface User { // 1文字目は大文字(Userのこと)
userName: string; // 頭にreadonlyを付けることも可
score: number;
// email?: string; として、あってもなくてもいい指定も可
// ↑ 使用例:登録画面などで、まだ情報が未入力な場合など
}
// const user: {userName: string; score: number} = {
const user: User = {
userName: 'Taro',
score: 80,
};
user.userName = 'Jiro'; // OK!
user.score = 90; // OK!
user.userName = true; // エラー
user.email = 'taro@example.com'; // エラー
}
- 関数の型付け:関数の引数名や返り値にも型付けができる。
function double(num: number): numberのような書き方は、TypeScriptの中でも最も基本的で重要な関数の型注釈の1つで、名前・年齢などのデータ処理を書くときに日常的に使われる型付けなので、今のうちに身につけておく
'use strict';
{
function double(num: number): number {
// 引数()は数値じゃないとダメ,
// 戻ってくる値も数値じゃないとダメ,という指定
return 'OK'; // 文字列なのでエラーになる
}
// ()内が引数(渡された値)
console.log(double(10)); // 型付けした数値はOK
console.log(double('Hello')); // 文字列はエラー
}
- 返り値がない関数の場合:
: voidを使う
'use strict';
{
function printUserName(userName: string): void {
console.log(userName); // OK!
return 'OK'; // エラー
}
}
- 関数式の型付け:変数に関数を代入する時も型を明示できる。関数を引数として渡す時(コールバック関数など)や後から中身を決めたい時に使用され、“この変数にはこういう形の関数しか入れさせない” というTypeScriptの型安全性の強みがしっかり活きる書き方のため、覚えておく
'use strict';
{
// ① doubleという変数には「数値を受け取って
// 数値を返す関数しか入れられません」と定義
let double: (num: number) => number;
// ② そのルールに合う関数を代入
//(アロー関数でも普通のfunctionでもOK)
double = (num: number): number => {
return num * 2; // OK!
return 'OK'; // エラー
};
console.log(double(10)); // OK!
console.log(double('OK')); // エラー
}
- ジェネリクス:2つの関数があり、「型だけ違う」ので1つにまとめたいという時、
<T>「ジェネリクス型引数」=“仮の型名”を定義して、それを関数の引数や戻り値に使えるようにする
'use strict';
{
// 数値を2回表示する関数(numはnumber型と指定)
function printNumberTwice(num: number): void {
console.log(num);
console.log(num);
}
// 文字列を2回表示する関数(strはstring型と指定)
function printStringTwice(str: string): void {
console.log(str);
console.log(str);
}
// ジェネリクスを使った関数(Tは受け取った値の型)
function printTwice<T>(value: T): void {
// valueを2回表示する(受け取った型に応じて柔軟に動作)
console.log(value);
console.log(value);
}
// ジェネリクスの関数に number を指定 → T = number として動作
printTwice<number>(10); // 10が2回表示される
// ジェネリクスの関数に string を指定 → T = string として動作
printTwice<string>('OK'); // 'OK' が2回表示される
}
Tailwind CSS入門(全14回)
- CSSフレームワーク:CSS を一行も書くことなく、HTML の class 属性で特殊なキーワードを指定するだけで、こうしたボタンなどのコンポーネントをいい感じにスタイリングしてくれる→その中でも、'小さなスタイルを組み合わせる方式'として存在するのがTailwind CSS。HTML直書きの
style属性だけでは実現できない:hoverや:focusなどの擬似クラスやメディアクエリーに対応したスタイルも設定できるようになる -
リセットCSSの設定は不要!!
- ☆Tailwind CSSの使い方:➀
index.htmlを作りscriptタグ配置 - ➁VSCodeの拡張機能で「Tailwind CSS IntelliSense」をインストール
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Tailwind CSS</title>
<script src="https://cdn.tailwindcss.com"></script>
<!-- Tailwind CSS の scriptタグ -->
</head>
<body>
<h1 class="text-right">Hello</h1>
<!-- テキストを右に寄せる設定 -->
</body>
</html>
- ➂動作させるためのファイル
tailwind.config.jsを作成(中身は空でOK)
ここまで作れば、予測変換でTailwindを出してくれる - Tailwind導入部分をホバーすると、CSS仕様が表示される
- スタイルは空白で区切って複数指定することができるが、同じ種類のスタイルをうっかり複数設定してしまった場合、拡張機能が'バッティングしている'という警告を出してくれる。(例:
text-leftとtext-center)
<body>
<h1 class="text-center text-4xl font-bold">Hello</h1>
<!-- text-4xl → font-sizeとline-heightが同時設定されている -->
</body>
- Tailwind CSSで使える色:以下のURLからチェック
<body>
<h1 class="text-center text-4xl font-bold text-red-500 opacity-50">Hello</h1>
<!-- 色や不透明度も設定できる -->
</body>
- 背景色は
bg-、幅はw-で設定する
<body>
<div class="bg-red-400 w-40 h-40">Box 1</div>
<!-- 0.25rem × 40 = 10rem = 160pxという rem基準の独自の固定幅 -->
<div class="bg-sky-400 w-[160px]">Box 2</div>
<!-- w-[160px]=Box 1の固定幅を分かりやすくpx指定 -->
<div class="bg-orange-400 w-3/4">Box 3</div>
<!-- 幅を「w-3/4」で指定 → 親要素の幅の75%を占めるという意味 -->
<div class="bg-green-400 w-[75%]">Box 4</div>
<!-- w-[75%]=Box 3のサイズを分かりやすく%指定 -->
</body>
- ☆スケール値(優先度:高)
| クラス名 | 計算式 | 結果 | 用途イメージ |
|---|---|---|---|
w-1 |
0.25rem × 1 | 0.25rem(= 4px) | ほぼ極小、点や余白など |
w-2 |
0.25rem × 2 | 0.5rem(= 8px) | 余白やアイコンマージンなど |
w-4 |
0.25rem × 4 | 1rem(= 16px) | 小さいボタンやスペーサー |
w-10 |
0.25rem × 10 | 2.5rem(= 40px) | アイコンサイズなど |
w-40 |
0.25rem × 40 | 10rem(= 160px) | 小箱・カードなど |
w-96 |
0.25rem × 96 | 24rem(= 384px) | 大きめのレイアウト用 |
- ☆分数(割合)指定(優先度:中 ※使う場面による)
| クラス名 | 意味 | pxではなく比率 |
|---|---|---|
w-1/2 |
親幅の 1/2 | 50% |
w-1/3 |
親幅の 1/3 | 約33.3333% |
w-2/3 |
親幅の 2/3 | 約66.6666% |
w-1/4 |
親幅の 1/4 | 25% |
w-3/4 |
親幅の 3/4 | 75% |
-
marginpaddingの指定:以下の画像を参考のこと -
t:top、l:left、r:right、b:bottom、X:X軸、y:Y軸
<body>
<!--
m-4:marginを全方向に 0.25rem × 4 = 1rem(=16px)
→ Box 1の外側に16pxの余白が付く
-->
<div class="bg-red-400 w-40 h-40 m-4">Box 1</div>
<!--
mb-4:margin-bottom に 0.25rem × 4 = 1rem(=16px)
→ Box 2の下に16pxの余白が空く(下のBox 3と離れる)
-->
<div class="bg-sky-400 w-[160px] mb-4">Box 2</div>
<!--
mx-auto:margin-left & margin-right を自動設定
→ 横幅が固定や割合指定の要素を中央に寄せたい時に使う
(例:w-3/4 は親要素の75%なので、中央寄せされる)
-->
<div class="bg-orange-400 w-3/4 mx-auto">Box 3</div>
<!--
pl-4:padding-left に 0.25rem × 4 = 1rem(=16px)
→ Box 4内のテキストが左から16px分ずれて表示される
-->
<div class="bg-green-400 w-[75%] pl-4">Box 4</div>
</body>
- ※
-4の理屈はスケール値の項と同じ
| クラス名 | 計算式 | px換算 |
|---|---|---|
-1 |
0.25rem × 1 | 4px |
-2 |
0.25rem × 2 | 8px |
-4 |
0.25rem × 4 | 16px ←今回はこれ |
-10 |
0.25rem × 10 | 40px |
-40 |
0.25rem × 40 | 160px |
-
borderの設定
<body>
<!--
border-solid:実線の枠線を指定(border-style: solid)
border-gray-200:薄いグレー色の枠線(border-color: #E5E7EB など)
border-8:全方向の枠線幅を 0.5rem × 8 = 4rem(=32px)に設定
※ border系は 0.125rem × n のスケールではなく、Tailwind独自の値で
`border-8` = 8px(ドキュメントでは直接「8px」と表記されている)
w-20:幅 5rem = 80px(0.25rem × 20)
h-20:高さ 5rem = 80px
-->
<div class="bg-red-400 w-20 h-20 border-solid border-gray-200 border-8">Box 1</div>
<!--
border-dashed:破線の枠線(border-style: dashed)
border-gray-200:色は同じく薄いグレー
border-b-8:下側(bottom)だけ border を 8pxに設定
-->
<div class="bg-sky-400 w-20 h-20 border-dashed border-gray-200 border-b-8">Box 2</div>
</body>
- ボタンのスタイリング
<body>
<!--
・背景色:bg-orange-400 → 中程度のオレンジ (#fb923c)
・文字色:text-white → 白文字
・外側余白:m-4 → 1rem(=16px)のマージン(上下左右すべて)
・上下パディング:py-2 → 0.5rem(=8px)
・左右パディング:px-4 → 1rem(=16px)
・角丸:rounded-full → 全体を円形にする最大の丸み(Pill型ボタンに)
・影色:shadow-black/30 → 黒の30%透明度で影を設定
・影の強さ:shadow-md → 中程度の影(深さやぼかし具合を調整)
-->
<button class="bg-orange-400 text-white m-4 py-2 px-4 rounded-full shadow-black/30 shadow-md">
Buy now!
</button>
</body>
| クラス名 | 内容の概要 | 見た目のイメージ |
|---|---|---|
shadow-none |
影なし | 平坦な見た目 |
shadow-sm |
ごく薄い影 | ほぼフラット、微小な浮き感 |
shadow |
通常の影(デフォルト) | 軽く浮いた印象 |
shadow-md |
中程度の影 | ややくっきりした立体感 |
shadow-lg |
やや大きな影 | しっかりとした浮き感 |
shadow-xl |
大きな影 | 明確な立体感、大きめのぼかし |
shadow-2xl |
さらに大きな影 | より深い立体感 |
shadow-inner |
内側に向けた影(凹んだ印象) | ボックスがくぼんで見える |
- マウスホバーした時のスタイル切替
- 下記をコーディングすると(マウス操作時) →
通常時:オレンジ色で影付きの丸ボタン
ホバー時:少し薄くなり、なめらかに色が変わる
クリック時(active):ちょっとだけ「沈む」ような動きで立体感UP
<body>
<!-- bg-orange-400:背景色オレンジ(やや薄め)
text-white:テキスト色白
m-4:全体の外側マージン1rem = 16px × 4 = 64px
py-2:上下パディング0.5rem = 8px
px-4:左右パディング1rem = 16px
rounded-full:角を完全に丸くする(pill型ボタン)
shadow-black/30:30%透明の黒い影(シャドウ色指定)
shadow-md:中くらいの影(適度に立体感)
hover:opacity-80:ホバー時に不透明度を80%に(薄くなる)
transition:状態変化にアニメーションを適用
duration-500:アニメーション時間500ms(0.5秒)
active:translate-y-1:クリック中にY軸方向に1単位(0.25rem=4px)下にずらす
-->
<button class="bg-orange-400 text-white m-4 py-2 px-4 rounded-full shadow-black/30 shadow-md hover:opacity-80 transition duration-500 active:translate-y-1">
Buy now!
</button>
</body>
- フレックスボックスの設定
<body>
<header class="bg-gray-200">
<!-- 背景色をgray-200(とても薄いグレー)に設定したヘッダー -->
Header
</header>
<div class="flex gap-4">
<!-- flexコンテナを作成し、子要素を横並び(flex-row)にする -->
<!-- gap-4 = 子要素の間に1rem(=16px)の隙間をつける -->
<main class="bg-gray-300 flex-1">
<!-- 背景色をgray-300(やや薄いグレー)に -->
<!-- flex-1 = 空きスペースをすべてこの要素に割り当てる(柔軟に伸縮) -->
Main
</main>
<aside class="bg-gray-400 w-40">
<!-- 背景色をgray-400(中間のグレー)に -->
<!-- 固定幅:w-40 = 10rem = 160pxの幅を指定 -->
Aside
</aside>
</div>
<footer class="bg-gray-500">
<!-- 背景色をgray-500(濃いグレー)に設定したフッター -->
Footer
</footer>
</body>
| パターン | Tailwindクラス例 | 主な用途・説明 | 頻出度 |
|---|---|---|---|
| 横並び | flex |
デフォルトで横並び(flex-row) |
★★★★☆ |
| 縦並び | flex flex-col |
子要素を縦方向に並べる(フォームやモバイル表示など) | ★★★☆☆ |
| 中央揃え(縦横) | flex justify-center items-center |
ボタン・モーダル・ローディングUIのセンター配置に便利 | ★★★★★ |
| 左右に分ける(余白あり) | flex justify-between |
ヘッダーのロゴとナビゲーション、カード内上下左右など | ★★★★☆ |
| 右寄せ | flex justify-end |
ヘッダー内ボタンやメニュー配置など | ★★☆☆☆ |
| 左寄せ(デフォルト) | flex justify-start |
デフォルト(わざわざ書く必要はあまりない) | ★☆☆☆☆ |
| 均等配置 |
flex justify-around / justify-evenly
|
複数ボタンやアイコンの等間隔配置 | ★★☆☆☆ |
| 縦方向中央揃え | items-center |
高さを揃える(親にflexが必要) |
★★★☆☆ |
| 縦方向上揃え | items-start |
左上に揃えたいときに使用 | ★★☆☆☆ |
| 縦方向下揃え | items-end |
例えばボタンをカードの底辺に揃えたいとき | ★☆☆☆☆ |
| ラップ折り返し | flex flex-wrap |
アイテム数が多くても折り返して表示 | ★★★☆☆ |
| 逆順(横 or 縦) |
flex-row-reverse / flex-col-reverse
|
要素の順番を反転したいとき(スマホ版表示など) | ★☆☆☆☆ |
- ☆よく使う組み合わせ
| 用途 | Tailwindクラス例 |
|---|---|
| ボタンの中央揃え | flex justify-center items-center |
| ロゴとナビ | flex justify-between items-center |
| フォームの縦並び | flex flex-col gap-2 |
| グリッド代替 |
flex flex-wrap gap-4(グリッド代わりに使える) |
- レスポンシブデザイン
<body class="bg-red-200 sm:bg-red-300 lg:bg-red-400">
<!-- 最初は赤(200)、画面幅が640px以上なら赤(300)、1024px以上で赤(400)に変化 -->
<header class="bg-gray-200">
Header
<nav class="sm:hidden">SP Menu</nav>
<!-- 640px以上では非表示(PCで非表示)、それ未満で表示(スマホで表示) -->
<nav class="hidden sm:block">PC Menu</nav>
<!-- デフォルトで非表示、640px以上で表示(PCで表示) -->
</header>
<div class="sm:flex sm:gap-4">
<!-- スマホ以上でflex化・間隔4(16px)を設定。スマホ未満では縦並びのまま -->
<main class="bg-gray-300 sm:flex-1">
Main
<!-- スマホ以上でMainに可変幅を持たせて、Asideと横並びになる -->
</main>
<aside class="bg-gray-400 sm:w-40">
Aside
<!-- スマホ以上で幅を固定(w-40 → 160px) -->
</aside>
</div>
<footer class="bg-gray-500">
Footer
</footer>
</body>
- ブレークポイント一覧(画面幅の基準)
- モバイルファースト設計 → 最初に“すべての画面”に適用されるクラスを書いて、sm:以降で“広い画面での上書き”を行う
| 表記 | 読み方 | 適用される最小幅 | 目安 |
|---|---|---|---|
sm: |
スモール | min-width: 640px |
スマホ横・タブレット縦 |
md: |
ミディアム | min-width: 768px |
タブレット横 |
lg: |
ラージ | min-width: 1024px |
ノートPC/小型デスクトップ |
xl: |
エクストララージ | min-width: 1280px |
デスクトップワイド画面 |
2xl: |
ダブルエクストラ | min-width: 1536px |
超ワイド・大画面 |
- 表示の切替(表示/非表示)
| Tailwindクラス | 意味 | 例 |
|---|---|---|
hidden |
非表示(すべての画面で) | <nav class="hidden"> |
sm:hidden |
スマホ以上で非表示 | <nav class="sm:hidden"> |
sm:block |
スマホ以上で表示 | <nav class="hidden sm:block"> |
md:flex |
タブレット以上でflexレイアウト | <div class="md:flex"> |
- レイアウト変更
| Tailwindクラス | 意味 | 用途例 |
|---|---|---|
sm:flex |
スマホ以上で横並び | カラムレイアウト |
sm:grid |
スマホ以上でグリッド表示 | 複数列のカード表示 |
sm:gap-4 |
要素間に16pxの余白 |
flexやgridと組み合わせる |
sm:flex-1 |
可変幅にする(flex内で) | メインコンテンツ領域など |
- 幅・高さ・余白の変更
| Tailwindクラス | 意味 | 用途例 |
|---|---|---|
sm:w-40 |
スマホ以上で幅160px | サイドバー、画像枠 |
md:w-full |
タブレット以上で全幅 | レスポンシブ画像 |
sm:mx-auto |
スマホ以上で中央寄せ(左右) | センター配置 |
sm:p-4 |
スマホ以上で内側余白16px | セクションの内側余白 |
- テキストや色の変更
| Tailwindクラス | 意味 | 用途例 |
|---|---|---|
text-sm / sm:text-base
|
スマホ以下で小さく、スマホ以上で標準に | フォントサイズ調整 |
bg-red-200 sm:bg-red-300 |
スマホ以上で色変更 | 背景色の変化で視覚強調 |
- 角丸のサイズ指定
| クラス名 | 角丸のサイズ |
|---|---|
rounded-none |
0px(角丸なし) |
rounded-sm |
2px(小さめ) |
rounded |
4px(デフォルト) |
rounded-md |
6px(中くらい) |
rounded-lg |
8px(やや大きい) |
rounded-xl |
12px(大きい) |
rounded-2xl |
16px(さらに大きい) |
rounded-3xl |
24px(超大きい) |
rounded-full |
完全な円形 |
Next.js入門(全18回)
- ブラウザからサーバー側の処理まで一貫して Reactで記述できる
- Next.jsの機能は多岐に渡るが、まずは、データを元にユーザーの一覧を表示して、クリックすると URL が切り替わってユーザーの詳細ページが表示されるようなシンプルなサイトを作っていく
→ プロジェクトのセットアップ、ルーティング、ページの作成、スタイリング、リンクの設定、データの埋め込みなど、Next.js を活用するにあたって必要となる、さまざまなトピックを学べる
- 導入手順
① Powershellを開きnpx -y create-next-app@14と入力
② プロジェクト名を入れる
③ TypeScriptを使うか?:yes
④ ESLint(構文チェック)するか?:yes
⑤ TailwindCSSを使うか?:yes
⑥srcディレクトリを使うか? 慣れないうちは:no
⑦ App Routerを使うか?:yes
⑧ importエイリアスをカスタマイズするか? no
(@/components/〜などのように、importパスを簡潔に書ける設定)
⑨ インストールされるので、フォルダが出たらVSCodeにドラッグ - ※基本的には
appフォルダとpublicフォルダのみを使う。
app:アプリケーションのコードを入れる。ほぼここを編集する
puvlic:デフォルトでsvg画像が入っているフォルダ
⑩ 開発用サーバーを起動する
ターミナルにnpm run devと入れるとlocalhost:3000のURLが出る
⑪ ブラウザにコピー → 「Next.js...by Vercel」という画面が出ればOK
-
Appフォルダ内、拡張子がtsxのpage.tsxファイル(=JSXファイルの TypeScript版)のreturn()内の書き換えを行う
export default function Home() {
return (
<h1>Users</h1>
)
}
- コード冒頭の
import Image from 'next/image';は削除 - ↓ こんな画面になったらOK

-
layout.tsx(ページ全体のレイアウトコード)も編集する -
<html lang="en">→ja(日本語)にする -
inter(フォントに関する設定)が付くコードは今回使わないため削除 -
titleを任意のものに編集する
import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
title: 'My Next.js App',
description: 'Generated by create next app',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<body>{children}</body> {/* ここのclassにもinnerがあったため削除 */}
</html>
);
}
-
globals.css(詳細なレイアウトコード)も編集する - 冒頭3行の
@tailwind…以外は削除してスタイルをリセットする
@tailwind base;
@tailwind components;
@tailwind utilities;
- 引き続き
page.tsxの編集 - フラグメントで囲ったあとに、
h1の下にul要素を配置
export default function Home() {
return (
<>
<h1>Users</h1>
<ul>
<li>User</li>
<li>User</li>
<li>User</li>
</ul>
</>
)
}
-
layout.tsxの全体レイアウトへTailwindCSSを実装する -
<body className="p-4">で全方向にpadding
import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
title: 'My Next.js App',
description: 'Generated by create next app',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<body className="p-4">{children}</body> {/* 追記部分 */}
</html>
);
}
-
page.tsxのh1ulへTailwindCSSを実装する -
<h1 className="text-lg border-b pb-1 mb-1">:文字を大きくするにはtext-lg、下にborderを引くにはborder-+bottomを意味するb - 下方向の
paddingはpb-、下方向のmarginはmb- - 先頭にリストを付けるには
list-disc+左方向のmarginはml-とする
export default function Home() {
return (
<>
<h1 className="text-lg border-b pb-1 mb-1">Users</h1> {/* 追記部分 */}
<ul className="list-disc ml-4"> {/* 追記部分 */}
<li>User</li>
<li>User</li>
<li>User</li>
</ul>
</>
)
}
- ユーザー一覧を表示するページ→リンクを貼って別ページに飛ばすには
-
localhost:3000/products/newのような、URL通りのファイル構成で実装する→ルーティングという -
ダイナミックルーティング:例えば
usersページのルーティングを連番で付けたい場合は、1つずつ作るのは大変なので、app/users/[userId]/page.tsxのように[]を使用して管理する - →
appフォルダの中にusersフォルダを作り、その中に[userId]というフォルダ、その中にpage.tsxを作る - 元々あった
page.tsxの中身を新規の方へコピペする - 結果のページを見るにはURL
localhost:3000/users/1で入力する - 連番の数をページ表示するには、
UserPage(props)として引数追加し、コード上部に、tsのオブジェクト型付けとしてInterface~を定義 -
UserPage(props)に、オブジェクト型付け部分のPropsを渡す=UserPage(props: Props) - 表示部分は
User Page {props.params.userId}とする
interface Props { // tsのオブジェクト型付け(PropsのP:1文字目は大文字)
params: { userId: string };
} // ここは'そういうもの'として覚える(paramsとか)
export default function UserPage(props: Props) { // 引数+型付け追加
return (
<>
<h1 className="text-lg border-b pb-1 mb-1">
User Page {props.params.userId} {/* 構成の書き方を覚える */}
</h1>
</>
)
}
- ↓
User Pageの後に番号が追加されて表示される
- ユーザーの一覧からリンクを貼る
→<a>タグはページ全体に再読み込みがかかってしまい、使用を避けたいので、最小限の読み込みだけで次のページに遷移できるように、Linkという特殊なコンポーネントを使用する - まずは、
usersフォルダに入っていない方(appフォルダ直下)のpage.tsxを編集。<li>User</li>の前に<Link>と打つと、候補にnext/linkと出るのでそれを選択すると、コードの冒頭にimport Link from "next/link"(必要なコンポーネント読み込み)が自動生成される - 最初の
Link先は"/users/0"とし、各種Tailwindの設定も行う - Tailwind:
className="text-blue-500 hover:text-blue-700"→リンク用に文字色を青にし、ホバーすると色が濃くなるスタイル -
<Link>タグを<li>タグの中に入れて、usersの連番12もコピー
// `app`フォルダ直下の`page.tsx`
import Link from "next/link" // 生成されたLinkコンポーネント
export default function Home() {
return (
<>
<h1 className="text-lg border-b pb-1 mb-1">Users</h1>
<ul className="list-disc ml-4">
<li>
<Link href="/users/0" className="text-blue-500 hover:text-blue-700">
User 0
</Link> {/* <li>タグの中に<Link>を入れる */}
</li>
<li>
<Link href="/users/1" className="text-blue-500 hover:text-blue-700">
User 1 {/* 連番でコピーする */}
</Link>
</li>
<li>
<Link href="/users/2" className="text-blue-500 hover:text-blue-700">
User 2
</Link>
</li>
</ul>
</>
)
}
- そして、
users/[userId]フォルダ内のpage.tsx=各種リンク先に「戻る」ボタンを設置する -
h1の下にpタグを書き、その中にLinkを配置+Tailwindも設定。先ほどのpage.tsxに同じく、コードの冒頭にimport Link from "next/link"が生成されたことを確認する
// `users/[userId]`フォルダ内の`page.tsx`
import Link from "next/link"; // 生成されたLinkコンポーネント
interface Props {
params: { userId: string };
}
export default function UserPage(props: Props) {
return (
<>
<h1 className="text-lg border-b pb-1 mb-1">
User Page {props.params.userId}
</h1>
<p>
<Link href="/" className="text-blue-500 hover:text-blue-700">Go back</Link>
{/* <Link>追記。href="/"とし、表示メッセージは「Go back」とする */}
</p>
</>
)
}
- ユーザーのデータを保持し、それを元にコンテンツを組み上げる
-
appフォルダ内に、'library'を意味するlibフォルダを作成し、ファイル作成。単にデータを保持するだけで JSX は使わない、+ TypeScript で記述したいので、名前はusers.tsとする - 配列で各ユーザーのデータを作り
interfaceでの型付けも忘れずに行う -
users.tsを使用してユーザーの一覧を作成するために、const users: User[]の冒頭にexportを付ける
// libフォルダ内に新規作成の`users.ts`
interface User { // 型付け
id: number;
name: string;
prof: string;
}
export const users: User[] = [ // 各ユーザーのデータ
// データを他で使用するために`export`を付ける
{id: 0, name: 'Taro', prof: 'He is good'},
{id: 1, name: 'Jiro', prof: 'He is cool'},
{id: 2, name: 'Saburo', prof: 'He is smart'},
];
-
appフォルダ直下のpage.tsxにて、returnの上にusersItemを定義。usersと打つと入力候補に./lib/usersと出るのでこれを選択する。mapでアロー関数を書き、returnにli要素をコピーしてくる -
Link href="/users/0"としていた部分を React仕様に書き換える。属性の値を埋め込むには""は不要で=の直後に{}の中に埋め込んで書かないといけないので、テンプレートリテラルで書く - リンクテキストはユーザーの名前を埋め込む。
{user.name} - 連番になっていた下部の
Linkコードは削除し{userItems}を埋め込む
// `app`フォルダ直下の`page.tsx`
import Link from "next/link" // 生成されたLinkコンポーネント
import { users } from "./lib/users"; // 生成されたusersコンポーネント
export default function Home() {
const userItems = users.map((user) => { // 新規追加
return (
<li key={user.id}>
<Link href={`/users/${user.id}`} className="text-blue-500 hover:text-blue-700">
{/* `Link href="/users/0"`をReact仕様に書き換え */}
{user.name} {/* リンクテキストにユーザーの名前を埋め込む */}
</Link>
</li>
);
});
return (
<>
<h1 className="text-lg border-b pb-1 mb-1">Users</h1>
<ul className="list-disc ml-4">
{userItems} {/* 移動させたLink部分の埋め込み */}
</ul>
</>
)
}
- URLで渡された
userIdを元に、表示するユーザーのデータを取得する -
users/[userId]フォルダ内のpage.tsx=各種リンク先ページの編集 - 先ほど同様、
constでuserを定義し、usersと打つと入力候補に@/app/lib/usersと出るので選択。import { users } from "@/app/lib/users";が冒頭に生成されることを確認する -
userにprops.params.userIdを渡す。そのまま書くと、userIdに型付けしてあるstringの定義を脱するので、Number()で囲む -
pタグで新規に「戻る」機能を追記。Tailwindも忘れずに - 存在しないデータにアクセスした場合のエラー画面を
if文で作る。notFoundと打つと入力候補にnext/navigationと出るので選択する。
// `users/[userId]`フォルダ内の`page.tsx`
import { users } from "@/app/lib/users"; // 生成されたusersコンポーネント
import Link from "next/link"; // 生成されたLinkコンポーネント
import { notFound } from 'next/navigation'; // 生成されたnotFoundコンポーネント
interface Props {
params: { userId: string };
}
export default function UserPage(props: Props) {
const user = users[Number(props.params.userId)]; // 新規追加
// そのまま書くと`string`の定義を脱するので、`Number()`で囲む
if (user === undefined) {
notFound(); // 入力候補の`next/navigation`を選択
} // 存在しないデータにアクセスした場合のエラー処理
return (
<>
<h1 className="text-lg border-b pb-1 mb-1">
{user.name} {/* 新定義した部分の埋め込み */}
</h1>
<p>
{user.prof} {/* 新定義した部分の埋め込み */}
</p>
<p className="mt-4"> {/* 新規追加:「戻る」機能 */}
<Link href="/" className="text-blue-500 hover:text-blue-700">Go back</Link>
</p>
</>
)
}
-
http://localhost:3000/users/99など設定していないユーザーIDを含むURLを入れ、↓ 以下のようなページに飛べばOK
- 上記エラーページを別途 作成してカスタマイズする
-
usersフォルダ(直下)の中にnot-found.tsxというファイルを作成し、users/[userId]フォルダ内のpage.tsxの内容をコピーし編集する - 最初の
importコードはLink以外を削除する -
propsを渡される予定もないので、interface Props { params: { userId: string }; }やprops: Propsも削除 - 単にJSXを返せばいいため、
return上の
const user = users[Number (props.params.userId)]; if (user === undefined) { notFound(); }も削除する - コンポーネント名を分かりやすく
UserNotFoundPageに変える
// `users`フォルダ内の`not-found.tsx`
import Link from "next/link"; // Linkだけ残して あとは消す
export default function UserNotFoundPage() {
// UserPage → UserNotFoundPage にコンポーネント名を変更
return ( // ↑ JSX部分以外もあらかた削除
<>
<h1 className="text-lg border-b pb-1 mb-1">
Error! {/* 表示メッセージ修正 */}
</h1>
<p>
User not found! {/* 表示メッセージ修正 */}
</p>
<p className="mt-4">
<Link href="/" className="text-blue-500 hover:text-blue-700">Go back</Link>
{/* ↑ ここはそのまま! */}
</p>
</>
)
}
- フレームワークの学習は'そういうもの'と割り切って学ぶこと。細かい疑問はいったん置いておいて、まずは今回作り上げたサイトを何も見ずに組み上げられるようになるまで練習を積んでみるのが良い







