Open47
「これからはじめるReact実践入門 コンポーネントの基本からNext.jsによるアプリ開発まで」の読書メモ
読もうとしたきっかけ
- 社内の勉強会でReact.jsを用いた社内開発ツールに関するLTがあり、将来的にこれを保守するとなると今の自分の理解度では到底無理そうだと分かったため、改めてReact.jsを勉強したかったから。
この本を読んで得たいこと
- React.js を用いたWebアプリケーションの保守(例: 不具合の原因特定および修正)ができるようになる
- FlutterでいうWidgetテスト相当のテストが書けるようになる
この本は、導入編・基本編・応用編の三編に分かれている
- 導入編
- フロントエンド開発の来歴
- Reactの特徴
- 環境構築
- モダンJavaScriptの基本
- Reactの基本概念(コンポーネント、Props、Stateなど)
- 基本編
- アプリ開発でよく利用する概念(フォーム開発、スタイル定義、組み込みコンポーネント、フックなど)
- Reactアプリ開発でよく利用する周辺ライブラリ
- 応用編
- ルーティング
- テストの仕組み
- TypeScriptでReactアプリを開発する方法
- Next.jsを使った応用アプリ開発
Chapter 1: イントロダクション
P10
- 部品化されたコンポーネントを組み合わせることで、ページを構成していくのが基本という考え方をコンポーネント指向と言う。
P15-16
- 開発時にはバンドルせず、モジュール個々のインポートをブラウザーに委ねるツールをノーバンドルツールと呼ぶ。
- ただし、完全にバンドルしないのでhなく、モジュールを大量に含んだライブラリを部分的にバンドルすることでインポートとバンドルのバランスを取っている。
- 本番リリースでは完全にバンドルすることが一般的。
- 例: Vite
P31
- オブジェクトリテラルの簡易構文
- 変数と同名のプロパティ
- プロパティと その値を格納した変数との名前が等しい場合は、値の指定を省略できる。
- 変数と同名のプロパティ
const title = 'sample';
const author = 'alice';
const book = {title, author};
// 上は下と等価
// const book = {title: title, author: author};
P31
- オブジェクトリテラルの簡易構文
- メソッドの簡易構文
- メソッドを「メソッド(){...}」のように書ける
- メソッドの簡易構文
const member = {
name: 'alice',
greet() {
console.log('Hello, ${this.name}');
}
// 上は下と等価
// greet: function() {
// console.log('Hello ${this.name}');
//}
}
P31
- オブジェクトリテラルの簡易構文
- 算出プロパティ
- プロパティ名をブラケットで囲むことでプロパティ名を生成できる
- 算出プロパティ
let i = 0;
const member = {
[`attr${++i}`]: 'alice',
[`attr${++i}`]: 18
}
console.log(member);
// {attr1: 'alice', attr2: 18}
P35
- 分割代入の複雑な例
const member = {
name: 'alice',
address: {
prefecture: '静岡県',
city: '藤枝市'
}
'sex': 'female',
'age': 18,
}
// member.sex の値を gender に代入する
const { sex: gender } = member;
console.log(gender); // female
// 残りのプロパティを取得する
cosnt {fullname, address, ...rest} = member;
console.log(fullname); // alice
console.log(rest); // { sex:'female', age:18}
// 入れ子のオブジェクトを分解する
const { address, address: {city}} = member;
console.log(address); // {prefecture:'静岡県', city:'藤枝市'}
console.log(city); // 藤枝市
P37
- 可変長引数は通常の引数と併用することも可能だが、その場合、引数リストの末尾に置く必要がある(可変長引数以降の引数がすべて吸収されるため)
- arguments を使って可変長引数を実装するのは古い書き方だからやめろ
// これはだめ
function hoge(...args, title){ ... }
// これはいい
function hoge(title, ..args){. .. }
P43
- モジュールをインポートする際には、読み込む側のjsファイルからの相対パスで表す。
- モジュール側でexportしていても、import側で明示的にインポートされなかったものはアクセスできない。
P44
- モジュール配下のすべてのメンバーをまとめてインポートする場合は、アスタリスク表現を使ってなおかつas句でモジュールの別名も指定する
import * as app from './App.js'
console.log(app.getTriangle(10, 2)); // 10
P44
- export default キーワードを利用することで、ひとつだけ既定のエクスポートを設定できる。
// Util.js
export default class Util {
static getCircleArea(radius){
return (radius * 2) * Math.PI;
}
}
// module_use_util.js
import Util, { getTriangle } from'./Util.js'
console.log(Util.getCircleArea(10));
P45-46
- 動的インポートを利用することで、必要に応じてインポートを行うことができる
// ./App.js を非同期にインポートして、インポートができ次第thenメソッド配下のコールバック関数を実行する
import('./App.js').then(app => {
console.log(app.getTriangle(10,5)); //25
const a = new app.Article();
console.log(a.getAppTitle());
});
Chapter2: Reactの基本
P52-53
- npm run build で /buildディレクトリ配下にビルド済みファイルが生成される。
- これらを公開フォルダに配置することでアプリの実行が可能。
P54
P56
- src/index.js 内にかかれている reportWebVitals(); を reportWebVitals(console.log); に書きかえることで開発者ツールのコンソールにパフォーマンス監視の情報をロギングできるようになる。
ref
P57-58
よくあるエラーとその対策
Target container is not a DOM element
- createRoot(getElementById)メソッドで指定したid値に誤りがないか確認する
Functions are not valid as a React child
- renderメソッドにJSX式以外を渡していないか確認する
何も表示されない
- createRoot と render がセットで呼び出されているか確認する
P59
- npm run start で実行した場合開発モード、npm run build でビルドされたアプリは本番モードで動作する。
- 開発モードと本番モードでの違いの例として、Strictモードは開発モードでしか動作しない。
P61-62
コンポーネントを定義する際のルール
- 関数名はPascalCaseであること
- PascalCase以外だとHTMLの標準タグと区別できないため正しく解釈されない
- 関数の戻り値はReact要素であること
- 関数を export すること
- 他のモジュールから呼び出せるようにするため
tips
- 複数行のJSX式はカッコで括る
// NG: 以下のような書き方だと、「return」で文の区切りとみなされ以降の式が評価されない
return
<div className="App">
...省略...
</div>;
// OK
return (
<div className="App">
...省略...
</div>
);
P64-66
- Reactでコンポーネントを定義するアプローチとして、関数コンポーネントとクラスコンポーネントの2種類がある
- React本家がリソースの主軸を関数コンポーネントに傾けているため、情報の得やすさを考慮して関数コンポーネントを選択するほうがベター
P66-69
JSXとHTMLの異なる点
唯一のルート要素を持つ
// NG
root.render(
<p>hoge</p>
<p>fuga</p>
);
// OK: <div></div>か<React.Fragment></React.Fragment>か<></>で囲む
root.render(
<>
<p>hoge</p>
<p>fuga</p>
</>
);
空要素は「~ />」で終える
// NG
const tag = <img src={image}>;
// OK: 子要素を持たない空要素は末尾を「/>」にする
const tag = <img src={image} />;
名前の異なる属性がある
- JavaScriptの予約語(例: for, class)は使えないため、代わりにhtmlFor, className を使う
- 複数の語で構成される属性(例: tabindex)は、camelCase(例: tabIndex)で表す
- aria-XXX, data-XXX などの例外はあるため注意
コメント構文が3種類ある
- JSX式で利用できるコメントは「//」「/* ~ */」「{/* ~ */}」の3種類のみ
- HTML標準のコメント構文「<!- ~ ->」は利用できないため、「{/* ~ */}」を代用する
- JavaScript標準のコメント構文は、タグの中のみで利用できる
P71-72
- {...} を使ってテキストを埋め込む際には内部でエスケープ処理がされる
- エスケープ処理したくない場合は、dangerouslySetInnerHTML={{_html:...}} とする
- あらかじめ適切なエスケープ処理がなされていることがわかっているコンテンツに対してだけ利用することを推奨
const content = `<h3>hoge</h3><img src="https://example.com/example.jpg" />`;
root.render(
<>
<p>{content}</p>
<p dangerouslySetInnerHTML={{_html:content}}></p>
</>
);
P73-74
- 文字列リテラルに含まれる実体参照は {...} でエスケープ処理されるので、エスケープシーケンスを使うことを推奨
// 以下の2つは等価
<div>foo & bar</div>
<div>{`foo \u0026 bar`}</div>
- {...}構文での true,false,undefined,null は出力されない。これらを表示したい場合は、String関数で文字列化する必要がある
P74-76
{...}構文で属性値を設定する際の注意点
属性値をクォートで囲まない
cosnt dest = 'https://example.com';
root.render(
<a href={dest + '/docs'}>hoge</a>
);
スプレット構文を使って、個々の属性に値を割り当てて回ることを避ける
const attrs = {
href: 'https://example.com',
target: '_blank',
};
root.render(
<a {...attrs}>fuga</a>
);
属性値の既定値はtrueになる
// 以下の2つは等価
<a href="index.html" download>foo</a>
<a href="index.html" download={true}>foo</a>
P76-78
- {...}構文でstyle属性を指定する場合は、「プロパティ名:値,...」のオブジェクト形式で指定する
- プロパティ名はcamelCaseにする(ハイフン形式のままにしたい場合はクォートで囲う)
- paddingやmargin などの数値を受け取るプロパティに対して、暗黙的に単位pxが付与される
- zindexやopacity などの単位が補完されない例外はある
const props = {
color: 'White',
'background-color': 'Blue',
padding: 3,
};
root.render(
<p style={props}>foo</p>
);
- スタイルの指定はclassName属性を使うことが一般的
- JavaScriptのコードの中にスタイル情報が混在することを避けるため
import './class.css';
{* 中略 *}
root.render(
<p className="hoge">foo</p>
);
// class.css
.hoge {
color: White;
background-color: Blue;
padding: 3px;
}
P79-80
- JSXからJavaScript本来のコード(React.createElementメソッドに変換される)
- createElement はタグ名、属性、子要素郡(可変長配列)を引数で受け取るため、入れ子のタグを表すことも可能になっている。
Chapter3: コンポーネント開発(基本)
P82
コンポーネント
定義
- ページを構成するUI部品
- テンプレートとそれに付随するロジックから構成される
利点
- コードの再利用が用意になる
- 部品の役割が明確になるため見通しが良くなる
P83-86
Props
- コンポーネントで値を扱うための仕組み
- コンポーネントにパラメータを渡すための引数
// FooBar.js
export default function FooBar(props) {
// 受け取ったProps値には「props.名前」の形式でアクセスできる
return (
<div>Hello {props.myName}</div>
);
}
// 以下のようにも書ける
// デフォルト値を指定できたり、コンポーネントに何を渡せばよいかが明確になる利点がある
// export default function FooBar({ myName = 'bob' }) {
// return (
// <div>Hello {myName}</div>
// );
// }
import FooBar from './FooBar'
// 中略
root.render(
// クォートで囲まれた属性値はすべて文字列としてみなされる
// 数字を数値として渡したい場合は、{...} を数字を囲む
<FooBar myName="alice" />
);
P83-90
State
- コンポーネントで値を扱うための仕組み
- コンポーネント内の状態を表す変数
- useState関数を使ってState値を初期化する
- State値を格納する変数fooに対して、それを更新するための関数はsetFooと命名するのが基本
// BarBaz.js
import { useState } from 'react';
export default function BarBaz({ init }){
// PropsでStateを初期化
const [count, setCount] = useState(init);
// ボタンクリック時にカウント値をインクリメント
const handleClick = () => setCount(count + 1);
return (
<>
<button onClick={handleClick}>foo</button>
<p>click count {count}</p>
</>
);
}
import BarBaz from './BarBaz';
// 中略
root.render(
<BarBaz init={0} />
);
P90
コンポーネントの再描画が発生するのは以下のタイミング
- Stateが更新された場合
- 渡されたPropsが変更された場合
- 親コンポーネントが再描画された場合
P100-101
- リストは以下の項目を変更する場合、key属性がないとReactは配列の変更を検知できないため、特定の項目が追加・削除された場合にリスト全体を再生成されるためパフォーマンス的に悪い。
- 配列要素が主キーを持つ場合はそれをkey属性にする、なければ生成されるリストの中で一意な値をkey属性に割り当てる。
P103
- 任意の条件式に基づいて既存の配列を絞り込むには Array#filterメソッドを使う
const foo = [
{
name: 'alice',
age: 30,
},
{
name: 'bob',
age: 19,
},
];
// age が 20未満の foo を抽出
const bar = foo.filter(f => f.age < 20); // [{name:'bob',age:19}]
P104
- 配列をソートするには Array#sortメソッドを使う
const foo = [
{
name: 'alice',
age: 30,
},
{
name: 'bob',
age: 19,
},
{
name: 'chris',
age: 29,
},
];
// 年齢が若い順でソート
const bar = foo.sort((m, n) => m.age - n.age);
// 逆順でソートする場合は以下のような感じ
// const bar = foo.sort((m, n) => n.age - m.age);
P105-110
条件分岐
if文
export default function Foo({ foo }){
let p;
if(foo.age < 20){
p = <p>bar</p>;
}else{
p = <p>baz</p>;
}
return (
{p}
);
}
即時関数
export default function Foo({ foo }){
return (
{(()=>{
if(foo.age < 20){
p = <p>bar</p>;
}else{
p = <p>baz</p>;
}
})()}
);
}
? A : B, &&, ||演算子
- 条件 ? A : B
- 条件の評価結果がtrueならA、falseならBを出力する
- 条件 && A
- 条件の評価結果がtrueならA、falseならnullを出力する
- 条件 || B
- 条件の評価結果がtrueならnull、falseならBを出力する
export default function Foo({ foo }){
return (
<>
{foo.age < 20 ? <p>bar</p> : <p>baz</p>}
{foo.age < 20 && <p>bar</p>}
{foo.age < 20 || <p>baz</p>}
</>
);
}
P114-116
- 呼び出し元要素配下のコンテンツを取得するには、props.children を使う
- children の実態は配下コンテンツを表すJSX要素の配列
- 標準的なHTML文字列の他にReactコンポーネントを指定しても構わない
// Foo.js
export default function Foo({children}){
return (
{children}
);
}
import Foo from './Foo';
// 中略
root.render(
<Foo>
<p>bar</p>
<p>baz</p>
</Foo>
);
P116-118
- 複数のchildrenを受け取りたい場合は、以下の2種類がある
Propsを利用するパターン
// Foo.js
export default function Foo({name, age}){
return (
<div>
{name}
<hr />
{age}
</div>
);
}
import Foo from './Foo';
// 中略
const name = <p>my name is alice</p>;
const age=<p>20</p>;
root.render(
<Foo name={name} age={age}></Foo>
);
childrenから目的の要素を取り出すパターン
// Foo.js
const name = children.find(e => e.key === 'name');
const age = children.find(e => e.key === 'age');
export default function Foo({children}){
return (
<div>
{name}
<hr />
{age}
</div>
);
}
import Foo from './Foo';
// 中略
root.render(
<Foo>
<p key='name'>my name is alice</p>
<p key='age'>20</p>
</Foo>
);
P119-121
- childrenに対してパラメータを引き渡したい場合は、呼び出し元配下のテンプレートをテンプレートを返す関数化する
- 描画のための関数を表すPropsを総称してRender Propsと呼ぶ
// Foo.js
export default function Foo ({ src, render }){
return(
<dl>
{src.map(elm => (
<React.Flagment key={elm.key}>
{render(elm)}
<React.Flagment />
))}
</dl>
);
}
import Foo from './Foo';
const foo = [
{
name: 'alice',
age: 30,
},
{
name: 'bob',
age: 19,
},
{
name: 'chris',
age: 29,
},
];
// 中略
root.render(
<Foo src={foo} render={elm=>(
<>
<dt>
<p>`${elm.name} ${elm.age}`</p>
</dt>
</>
)}/>
);
P129
- Reactではイベントハンドラーを終えたあとにStateに新しい値が反映される
- 例: 以下のコンポーネントの「tapme」と書かれたボタンを押しても count は1ずつしか増えない。
import { useState } from 'react';
export default function Foo({init}){
const [count, setCount] = useState(init);
const handleClick= () => {
setCount(count + 1);
setCount(count + 1);
};
return (
<>
<button onClick={handleClick}>tapme</button>
<p>{count}</p>
</>
);
}
P134-136
mouseenter/mouseleaveとmouseover/mouseoutの挙動の違い
- どちらも、要素に対してマウスポインターが出入りしたタイミングで発生するイベント
- 要素が入れ子かつイベントハンドラが外側の要素に対して設定されている場合に差異が出る
- mouseenter/mouseleave
- 対象となる要素の出入りに際してのみイベント発生
- mouseover/mouseout
- 内側の要素に出入りしたときにもイベント発生
- mouseenter/mouseleave
P139,143
- ReactのイベントオブジェクトはJavaScriptのイベントオブジェクトではなく、ブラウザ間の仕様差を吸収した合成イベント
- JavaScriptのイベントオブジェクトはnativeEventメンバーからアクセスできる
P144-147
- イベントハンドラに任意の引数を渡したい場合は、以下の2種類がある
イベントハンドラに独自の引数を追加する
- 実行時に引数の値が変化する場合に使う
export default function Foo(){
const myHandler =(e, type) => {
// 何らかの処理
}
}
return (
<div>
<button onClick={e => myHandler(e, 'foo')}>tapme</button>
// onClickには関数を渡す必要があるため以下のような書き方はできない
// <button onClick={myHandler(e, 'foo')}>tapme</button>
</div>
);
独自データ属性を使う
- あらかじめ引数の値が決まる場合に使う
- 独自データ属性とは、任意のタグにdata-xxx形式で指定できる属性
- xxxには、小文字英数字、ハイフン、ドット、コロンが使える
- 独自データ属性には要素オブジェクトからdatasetプロパティにアクセスする
export default function Foo(){
const myHandler =e => {
const type = e.target.dataset.type;
// 何らかの処理
}
}
return (
<div>
<button data-type="foo" onClick={myHandler}>tapme</button>
</div>
);
P147-156
イベントの伝播フロー
- イベント発生
- キャプチャフェーズ(最上位のwindowオブジェクトから下位要素にイベントを伝播)
- ターゲットフェーズ(イベントの発生元を特定)
- バブリングフェーズ(下位要素で発生したイベントを上位要素に伝播)
- 最上位のwindowオブジェクトまで伝播されれば終了
イベント処理の実行タイミング
- 伝播の改定で対応するハンドラが存在していれば実行される
- onClick は 既定ではバブリングフェーズでイベントが処理される
- キャプチャフェーズでイベント処理させたい場合は、onXxxの代わりにonXxxCaputure を使う
イベント処理実行後の挙動カスタマイズ
- イベント伝播を抑制したい場合は、イベント処理実行の前に event.stopPropagation() を実行する
- イベント既定の動作(例: 別ページへ遷移する)をキャンセルしたい場合は、イベント処理実行の前に event.preventDefault() を実行する
- ただし、キャンセルできるのはevent.cancelable が true のものだけ
非Passiveモードでイベントハンドラを設置する
※Passive モードとは、イベントハンドラが preventDefaultメソッドを呼び出さないことを予めブラウザに通知するモードのこと
- React における一部のイベントではPassiveモードが既定である
例: スクロールさせないようにする
- div要素のonWheelにイベントハンドラを渡すとエラーが出るため、div要素を取得してElement#addEventListenerメソッドを直接呼び出しPassiveモードをオフにする必要がある
- また、コンポーネント破棄時にイベントハンドラは明示的に除去する必要がある
import { useRef, useEffect } from 'react';
export default function Foo(){
const handleWheel = e => e.preventDefault();
const divRef = useRef(null);
useEffect(() => {
const div = divRef.current;
div.addEventListener('wheel', handleWheel, {passive:false});
return (() => {
div.removeEventListener('wheel', handleWheel);
});
});
return (
<div ref="divRef" className="box">
// 何かしらの要素
</div>
);
}