"Learning React" を読んだ
Alex Banks & Eve Porcello の “Learning React” Second Edition。
ざっと読んでみたが、この本は正直微妙だった。
というのも、React の公式ドキュメントを読めば十分だし、そちらの方が説明の漏れもない。最近のメジャーなプログラミング技術は、ウェブ上の公式ドキュメントが簡潔・正確・包括的で、それで十分ということが普通だけれど、それはそれとして良書は良書としてよいものだと思っている。しかし “Learning React” はそれでも評価しづらい。
“Learning React” という本のポイントとして、React Hooks を全面的に利用していて、クラスコンポーネントは非推奨としてまったく使わなくなっていることが挙げられる。一方で、公式ドキュメントは残念ながら、基本的にクラスコンポーネントを使った記法が残っている。その点で、Hooks を使った最新の書き方だけを学びたければ “Learning React” がよい、はずなんだけど、逆を言えばそれくらいしかこの本のメリットがない。それ以外は、たとえばコンポーネントのリストを生成するときに key
属性を付与すべき理由とか、説明が一部足りていないので、そもそも良書と言いづらい。
“Learning React” 独自に得られる話があるかというと、もちろんないわけでもないけれど、少ない。第2章で、ES6 以降の JavaScript の主な機能を紹介しているが、こういうのは JS 本で学ぶべき話。第3章で、関数敵プログラミングについて書かれているが、これも同様。第4章と第5章の React の基本的な話は、公式ドキュメントにもほぼ書かれているような内容で、深みもない。第6章と第7章のステート管理と Hooks の話は重要だが、公式ドキュメントも Hooks だけで複数のページが割かれていて、ここと次の第8章だけは公式ドキュメントより説明が豊富で、意味がある。ただ、説明のクオリティはそんなに高くない感じがする。第8章は API から取得したデータを実際に扱う話で、React そのものと直接関係はないが React を使う上で一緒に活用されることが多い話なので、たしかに1章を使うのはありがたい。第9章は React のアドバンスドな機能いくつかを紹介しているが、これは公式ドキュメントでもよい。第10章のテストの説明も公式ドキュメントでもまあいいかなという程度で、ESLint, Prettier の紹介とその設定ファイルの書き方の説明があるのはよい。第11章の React Router、第12章の React and the Server は公式ドキュメントで省かれている部分なので、よい。
そんな感じで、ところどころに価値がある箇所もあるけれど、少ない。いや、別に公式ドキュメントがあるからといっても、それを超えていなくても同レベルの内容であればよかったんだけど、公式ドキュメントに劣っているのがよくない。
以下、読書メモ。
1 Welcome to React
React チームの Pete Hunt が “Why React?” という記事を書いて、React への批判へのアンサーとした、とある。この “Why React?” という記事はどうやら今は消えているが、かつては React 公式ドキュメントの中にあった。
React で開発するときは React Developer Tools をインストールして使うことを薦めている。React コンポーネントツリーを調べたり、props/state を見たりすることができる。
React を使う場合、Node.js をインストールする必要がある。バージョンは 8.6.2 以上であることが望ましい。インストール方法は Node.js ウェブサイト に行けば分かる。Node.js をインストールすれば、Node.js のパッケージマネージャである npm もインストールされる。ただし、npm の代替として Yarn もある。
結局 npm を使うべきか Yarn を使うべきか分からない。今でも Yarn の方が速いらしいが、npm の速度もかなり改善されたらしいし、package-lock.json
ファイルも追加されたし、別に npm でもよさそうに思える。
2 JavaScript for React
各ブラウザの JavaScript サポート状況は kangax 互換性テーブル が参考になる。
この章では、ES6 以降の機能の中で、本書で使われるものを紹介する。
詳しくは David Flanagan の “JavaScript: The Definitive Guide (7th Edition)” などを参照。何気なく DOM の知識というか HTMLElement
クラスの知識が必要。
2.1 変数を宣言する
const
キーワードlet
キーワード- テンプレート文字列
${}
2.2 関数を作成する
- デフォルトパラメータ
- アロー関数
(...) => { ... }
2.3 JavaScript をコンパイルする
Babel のような JavaScript コンパイルツールを使って、最新の JavaScript 機能を含んだコードを古いブラウザでも対応したコードにコンパイルできる。
JavaScript コンパイル処理は、webpack や Parcel のようなビルドツールを使って自動化される。詳しくは後述される。
2.4 オブジェクトと配列
- 分割代入
{ bread, meat } = sandwich
- オブジェクトリテラル拡張
{ name, elevation }
- スプレッド演算子
...
2.5 非同期 JavaScript
fetch(url: string): Promise<Response>
API。async/await
関数。Promise
オブジェクト。
2.6 クラス
class Vacation {
constrcutor(destination, length) {
this.destination = destination;
this.length = length;
}
print() {
console.log(`${this.destination} will take ${this.length} days`);
}
}
const trip = new Vacation("Santiago, Chile", 7);
trip.print();
2.7 ES6 モジュール
text-helpers.js
:
export const print=(message) => log(message, new Date())
export const log=(message, timestamp) => console.log(`${timestamp.toString()}: ${message}`)
mt-freel.js
:
export default new Expedition("Mt. Freel", 2, ["water", "snack"]);
import { print, log } from "./text-helpers";
import freel from "./mt-freel";
print("printing a message");
log("logging a message");
freel.print();
CommonJS
Node.js は CommonJS モジュールパターンもサポートしている。
txt-helpers.js
:
const print=(message) => log(message, new Date())
const log=(message, timestamp) => console.log(`${timestamp.toString()}: ${message}`)
module.exports = {print, log}
const { print, log } = require("./txt-helpers");
3 Functional Programming with JavaScript
3.1 関数的が意味すること
3.2 命令的 vs 宣言的
3.3 関数的な概念
- 不可変
- 純粋関数
- データ変換
- 高階関数
- 再帰
- 合成
4 How React Works
4.1 ページセットアップ
ブラウザで React を動かすためには、2つのライブラリ React と ReactDOM が必要になる。React はビューを作成するライブラリで、ReactDOM は実際に UI をブラウザにレンダリングするライブラリ。両方とも unpkg CDN から取得できる。
React が動く HTML ページ:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>React Samples</title>
</head>
<body>
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script>
</script>
</body>
</html>
本にはないが、CDN Links - React によれば、crossorigin
を付けることをすすめている。
4.2 React 要素
SPA では DOM API を使って UI である HTML 要素を動的に作る。React では React.createElement
を使って React 要素を作ることができる。
React.createElement("h1", { id: "recipe-0" }, "Baked Salmon");
4.3 ReactDOM
ReactDOM の render
メソッドを使って、React 要素をレンダリングすることができる。
const dish = React.createElement("h1", { id: "recipe-0" }, "Baked Salmon");
ReactDOM.render(dish, document.getElementById("root"));
4.4 React コンポーネント
UI の作成を再利用可能・カプセル化した形にしたものが React コンポーネント。これは単に、React 要素を返す1引数をとる関数または render()
メソッドを実装したクラス。
// 関数コンポーネント。
function IngredientList(props) {
return React.createElement("ul",
{ className: "ingrediens" },
props.items.map((ingredient, i) => React.createElement("li", { key: i }, ingredient)));
}
// クラスコンポーネント。
class IngredientList extends React.Component {
render() {
return React.createElement("ul",
{ className: "ingrediens" },
this.props.items.map((ingredient, i) => React.createElement("li", { key: i }, ingredient)));
}
}
class
属性は、JavaScript で class
が予約語なので className
となっている。https://reactjs.org/docs/dom-elements.html#classname
本書によれば、クラスを使ったコンポーネント記法は deprecated になっていくと予告されている。
5 React with JSX
5.1 JSX としての React 要素
React 要素を作成するための簡潔な記法として JSX がある。
// JavaScript
React.createElement(IngredientList, {list: [...]});
// JSX
<IngredientList list={[...]}/>
JSX TIPS:
- コンポーネントのネストができる。
class
属性は予約語なので、className
が使われる。{...}
で JavaScript 式を評価してその結果が使われる。
5.2 Babel
Babel を使って JS を含む JavaScript コードを普通の JavaScript コードに変換できる。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>React Samples</title>
</head>
<body>
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script crossorigin src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
// ...
</script>
</body>
</html>
5.3 React フラグメント
複数の React 要素の列をまとめるとき、<div>...</div>
でまとめることもできるが不要なタグがたくさんできてしまう。そのために React.Fragment
タグを使うことができる。
function Cat({ name }) {
return (
<React.Fragment>
<h1>The cat's name is {name}</h1>
<p>He's good.</p>
</React.Fragment>
);
}
<React.Fragment>
は空タグ <>
で短縮して書くこともできる。
Ref: Fragments
5.4 webpack の紹介
webpack を使って、JSX/ESNext の変換や、JavaScript ライブラリの依存の管理、画像/CSS の最適化を行う。create-react-app や Gatsby などのツールを使えば、こうしたコンパイル作業は抽象化されて見えなくなる。
5.4.1 プロジェクトを作成する
mkdir recipes-app
cd recipes-app
npm init -y
npm install react react-dom serve
5.4.2 コンポーネントをモジュールに分ける
import React from "react";
import { render } from "react-dom";
import Menu from "./components/Menu";
import data from "./data/recipes";
render(<Menu recipes={data} />, document.getElementById("root"));
5.4.3 webpack ビルドを作成する
webpack.config.js
var path = require("path");
module.exports = {
devtool: "#source-map",
entry: "./src/index.js",
output: {
path: path.join(__dirname, "dist", "assets"),
filename: "bundle.js"
},
module: {
rules: [{ test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" }]
}
};
.babelrc
:
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
}
npm install --save-dev webpack webpack-cli
npm install --save-dev babel-loader @babel/core
npm install -save-dev @babel/preset-env @babel/preset-react
npx webpack --mode development
5.4.4 bundle をロードする
./dist/index.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>React Recipes App</title>
</head>
<body>
<div id="root"></div>
<script src="bundle.js"></script>
</body>
</html>
5.4.5 ceate-react-app
Create React App を使えば、webpack, Babel, ESLint などの設定を自動で行ってくれる。
npm install -g create-rect-app
create-react-app my-project
src/App.js
ファイルから JavaScript コードを修正する。
npm start
でアプリケーションを 3000 ポートで実行できる。
npm run build
でアプリケーションをビルドできる。
6 React State Management
コンポーネントには、プロパティ(引数)を通してデータを引き渡すことができる。一方、ステートを使って、データの変更を行うことができる。
6.1 スターレーティング・コンポーネントを作る
const Star = ({ selected = false }) => <FaStar color={selected ? "red" : "grey"} />;
export default function StarRating({ totalStars = 5 }) {
return [...Array(totalStars)].map((n, i) => <Star key={i} />);
}
6.2 useState フック
StarRating
コンポーネントをクリックして援交できるようにするためには、ステートを使って星の数を保存する。関数コンポーネントでステートを使うためには、フック機能を使う。React.useHook(default)
関数は配列を返し、配列の第1要素はステート変数で、配列の第2要素はステート値を変更する関数である。
Ref: Introducing Hooks
import React from "react";
const Star = ({ selected = false, onSelect = f => f }) => <FaStar color={selected ? "red" : "grey"} onClick={onSelect} />;
export default function StarRating({ totalStars = 5 }) {
const [selectedStars, setSelectedStars] = React.useState(3);
return (
<>
{[...Array(totalStars)].map((n, i) => (
<Star key={i} selected={selectedStars > i} onSelect={() => setSelectedSars(i + 1)} />
))}
<p>{selectedStars} of {totalStars} stars</p>
</>
);
}
以前のバージョンの React のコード:
export default class StarRating extends React.Component {
constructor(props) {
super(props);
this.state = {
starsSelected: 0
};
this.change = this.change.bind(this);
}
change(starsSelected) {
this.setState({ starsSelected });
}
render() {
const { totalStars } = this.props;
const { starsSelected } = this.state;
return (
<>
{[...Array(totalStars)].map((n, i) => (
<Star key={i} selected={selectedStars > i} onClick={() => this.change(i + 1)} />
))}
<p>{selectedStars} of {totalStars} stars</p>
</>
)
}
}
Note: HTML のイベントハンドラと違って、React では camelCase であり、かつ、関数を値に渡す、というのが重要なんだけれど、明記されていないような気がする。
6.3 再利用性を高めるリファクタリング
React では CSS スタイルを文字列でなく JavaScript オブジェクトで表現できる。style - DOM Elements - React
{...props}
という記法で、props をまとめて JSX のプロパティに引き渡すことができる。Spread Attributes - JSX In Depth - React
export default function StarRating({ style = {}, totalStars = 5, ...props }) {
const [selectedStars, setSelectedStars] = React.useState(3);
return (
<div style={{ padding: 5, ...style }} {...props}>
{[...Array(totalStars)].map((n, i) => (
<Star key={i} selected={selectedStars > i} onSelect={() => setSelectedSars(i + 1)} />
))}
<p>{selectedStars} of {totalStars} stars</p>
</div>
);
}
6.4 コンポーネントツリーのステート
たくさんのコンポーネントがステートを使うのはよいやり方じゃない。バグを追いかけたり変更をしたりするのが大変になるから。ステートは1ヶ所にまとめて管理するのがよい。そのための方法はいくつかあって、ここでは1つ目の方法を紹介する。コンポーネントツリーのルートにステートをもたせて、子コンポーネントには props を通して渡す。
“Color Organizer” アプリケーションを作るとする。色・そのタイトル・レーティングのリストを含んだサンプルのデータセット color-data.json
:
[
{
"id": "0175d1f0-a8c6-41bf-8d02-df5734d829a4",
"title": "ocean at dusk",
"color": "#00c4e2",
"rating": 5
},
{
"id": "83c7ba2f-7392-4d7d-9e23-35adbe186046",
"title": "lawn",
"color": "#26ac56",
"rating": 3
},
{
"id": "a11e3995-b0bd-4d58-8c48-5e49ae7f7f23",
"title": "bright red",
"color": "#ff0000",
"rating": 0
}
]
import React from "react";
import colorData from "./color-data.json";
function App() {
const [colors, setColors] = React.useState(colorData);
return (
<ColorList
colors={colors}
onRateColor={(id, rating) => {
setColors(colors.map(color => color.id === id ? { ...color, rating } : color))
}}
onRemoveColor={id => {
setColors(colors.filter(color => color.id !== id))
}}
/>
);
}
function ColorList({ colors = [], onRemoveColor = f => f, onRateColor = f => f }) {
if (!colors.length) return <div>No Colors Listed. (Add a Color)</div>;
return (
<div className="color-list">
{colors.map(color => (
<Color key={color.id} {...color} onRemove={onRemoveColor} onRate={onRateColor} />
))}
</div>
);
}
function Color({ id, title, color, rating, onRemove = f => f, onRate = f => f }) {
return (
<section>
<h1>{title}</h1>
<button onClick={() => onRemove(id)}>
<FaTrash />
</button>
<div style={{ height: 50, backgroundColor: color }} />
<StarRating selectedStars={rating} onRate={rating => onRate(id, rating)} />
</section>
);
}
公式ドキュメントでも、Lifting State Up として紹介されている方法。
6.5 フォームを作る
useRef
フックを使って ref を作ることで、DOM ノードを直接アクセスする。
Ref: useRef
txtTitle.current
, hexColor.current
は <input />
要素への参照になっている:
function AddColorForm({ onNewColor = f => f }) {
const txtTitle = useRef();
const hexColor = useRef();
const submit = e => {
e.preventDefault();
onNewColor(txtTitle.current.value, hexColor.current.value);
txtTitle.current.value = '';
hexTitle.current.value = '';
};
return (
<form onSubmit={submit}>
<input ref={txtTitle} type="text" placeholder="color title..." required />
<input ref={hexColor} type="color" required />
<buton>ADD</button>
</form>
);
}
controlled component では、React がフォームのステートをコントロールする:
function AddColorForm({ onNewColor = f => f }) {
const [title, setTitle] = useState('');
const [color, setColor] = useState('#000000');
const submit = e => {
e.preventDefault();
onNewColor(title, color);
setTitle('');
setColor('');
};
return (
<form onSubmit={submit}>
<input value={title}
onChange={event => setTitle(event.target.value)}
type="text"
placeholder="color title..."
required />
<input value={color}
onChange={event => setColor(event.target.value)}
type="color"
required />
<buton>ADD</button>
</form>
);
}
Uncntrolled Document - React によれば、基本的に controlled component を使うことを勧めている。では uncontrolled component はどういう場面で使うといいのか、不明。
6.6 React コンテキスト
コンポーネントツリーのルートにステートをまとめるのは、古いバージョンの React で広く使われてきたパターンだった。しかし、大規模アプリケーションになるとたくさんのステートを管理するのが大変になった。
React context がその代わりとなる手段になる。
Ref: Context - React
import React from 'react';
import colorData from './color-data';
import ReactDOM from 'react-dom';
const ColorContext = React.createContext();
function ColorProvider({ children }) {
const [colors, setColors] = React.useState(colorData);
const addColor = (title, color) => setColors([...colors, { id: uuid.v4(), rating: 0, title, color }]);
const rateColor = (id, rating) => setColors(colors.map(color => (color.id === id ? { ...color, rating } : color)));
const removeColor = id => setColors(colors.filter(color => color.id !== id));
return (
<ColorContext.Provider value={{ colors, addColor, removeColor, rateColor }}>
{children}
</ColorContext.Provider>,
)
}
function App() {
return (
<>
<AddColorForm />
<ColorList />
</>
);
}
function ColorList() {
const { colors } = React.useContext(ColorContext);
if (!colors.length) return <div>No Colors Listed. (Add a Color)</div>;
return (
<div className="color-list">
{colors.map(color => <Color key={color.id} {...color} />)}
</div>
);
}
function Color({ id, title, color, rating }) {
const { rateColor, removeColor } = React.useContext(ColorContext);
return (
<section>
<h1>{title}</h1>
<button onClick={() => removeColor(id)}>
<FaTrash />
</button>
<div style={{ height: 50, backgroundColor: color }} />
<StarRating selectedStars={rating} onRate={rating => rateColor(id, rating)} />
</section>
);
}
function AddColorForm() {
const [title, setTitle] = useState('');
const [color, setColor] = useState('#000000');
const { addColor } = React.useContext(ColorContext);
const submit = e => {
e.preventDefault();
addColor(title, color);
setTitle('');
setColor('');
};
return (
<form onSubmit={submit}>
<input value={title}
onChange={event => setTitle(event.target.value)}
type="text"
placeholder="color title..."
required />
<input value={color}
onChange={event => setColor(event.target.value)}
type="color"
required />
<buton>ADD</button>
</form>
);
}
ReactDOM.render(
<ColorProvider>
<App />
</ColorProvider>
document.getElementById('root')
);
7 Enhancing Components with Hooks
7.1 Introducing useEffect
React.useEffect()
を使って、レンダリングのたびに副作用を起こす関数(エフェクト)を登録できる。
function Checkbox() {
const [checked, setChecked] = React.useState(false);
React.useEffect(() => console.log(checked ? 'Yes, checked' : 'No, not checked'));
return (
<>
<input type='checkbox' value={checked} onChange={() => setChecked(checked => !checked)} />
{checked ? 'checked' : 'not checked'}
</>
);
}
7.2 The Dependency Array
useEffect()
を使うと、レンダリングのたびにエフェクトが呼ばれる。第2引数に依存配列 (dependency array) を渡すことで、指定した値が変更されたときだけエフェクトが呼ばれるようになる:
function App() {
const [val, setVal] = React.useState('');
const [phrase, setPhrase] = React.useStater('example phrase');
const createPhrase = () => {
setPhrase(val);
setVal('');
};
useEffect(() => { console.log(`typing "${val}"`); }, [val]);
useEffect(() => { console.log(`saved phrase: "${phrase}"`); }, [phrase]);
return (
<>
<label>Favorite phrase:</label>
<input value={val} placeholder={phrase} onChange={e => setVal(e.target.value)} />
<button onClick={createPhrase}>send</button>
</>
);
}
空の依存配列は、最初のレンダリングのときだけ呼び出される。
エフェクトが関数を返すと、その関数はコンポーネントがツリーから削除されたときに呼ばれる。つまり、クリーンアップに使える。
function useJazzyNews() {
const [posts, setPosts] = React.useSate([]);
const addPost = post => setPosts(allPosts => [post, ...allPosts]);
useEffect(() => {
newsFeed.subscribe(addPost);
return () => newsFeed.unsubscribe(addPost);
}, []);
useEffect(() => {
welcomeChime.play();
return () => goodbyeChime.play();
}, []);
return posts;
}
7.3 When to useLayoutEffect
useLayoutEffect()
は useEffect()
とは少し違うタイミングで呼び出される。通常は useEffect()
を使えばよいが、エフェクトがブラウザ描画に不可欠なものなら、useLayoutEffect()
を使う。
- レンダリングする。
useLayoutEffect
が呼ばれる。- ブラウザが描画する。コンポーネント要素が実際に DOM に追加されるとき。
useEffect
が呼ばれる。
function useWindowSize() {
const [size, setSize] = React.useState([0, 0]);
const resize = () => setSize([window.innerWidth, window.innerHeight]);
useLayoutEffect(() => {
window.addEventListener('resize', resize);
resize();
return () => window.removeEventListener('resize', resize);
}, []);
return size;
}
function useMousePosition() {
const [position, setPosition] = React.useState({x: 0, y: 0});
useLayoutEffect(() => {
window.addEventListener('mousemove', setPosition);
return () => window.removeEventListener('mousemove', setPosition);
}, []);
return position;
}
7.4 Rules to Follow with Hooks
- フックはコンポーネントのスコープの中でのみ動く。
- 機能ごとに複数のフックに分けるとよい。
- フックはトップレベルからのみ呼ぶべき。
7.5 Improving Code with Reducer
useReducer()
は、第1引数に reducer 関数をとり、第2引数に初期値をとる。reducer 関数は現在のステートを受け取り新しいステートを返す。
functin Checkbox() {
const [checked, setChecked] = React.useReducer(checked => !checked, false);
return (
<>
<input type="checkbox" value={checked} onChange={setChecked} />
{checked ? "checked" : "not checked"}
</>
);
}
7.5 useReducer to Handle Complex State
7.6 Improving Component Performance
7.7 shouldComponentUpdate and PureComponent
7.8 When to Refactor
8 Incorporating Data
8.1 Requesting Data
JavaScript では、HTTP リクエストを行う一番ポピュラーな方法は fetch を使うこと。
fetch(`https://api.github.com/users/moonhighway`)
.then(response => response.json())
.then(console.log)
.then(console.error);
これは URL https://api.github.com/users/moonhighway
への非同期リクエストを送る。レスポンスは .then(callback)
メソッドを使ってコールバックに渡され、レスポンスデータを JSON としてパースする。パースした JSON の結果をコンソールにログ出力する。何か問題が発生すれば、console.error
メソッドでエラー出力する。
function App() {
const [login, setLogin] = React.useState('moonhighway');
const [repo, setRepo] = React.useState('learning-react');
const handleSearch = login => {
if (login) return setLogin(login);
setLogin('');
setRepo('');
}
if (!login) return <SearchForm value={login} onSearch={handleSearch} />;
return (
<>
<GitHubUser login={login} />
<UserRepositories login={login} repo={repo} onSelect={setRepo} />
</>
);
}
function Fetch({ uri, renderSuccess, loadingFallback = <p>loading...</p>, renderError = error => (<pre>{JSON.stringify(error, null, 2)}</pre>)}) {
const [data, setData] = React.useState();
const [error, setError] = React.useState();
const [loading, setLoading] = React.useState();
React.useEffect(() => {
if (!uri) return;
fetch(uri)
.then(data => data.json())
.then(setData)
.then(() => setLoading(false))
.catch(setError);
}, [uri]);
if (loading) return loadingFallback;
if (error) return renderError(error);
if (data) return renderSuccess({ data });
}
function GitHubUser({ login }) {
const userDetails = ({ data }) => (
<div className="githubUser">
<img src={data.avatar_url} alt={data.login} style={{ width: 200 }} />
<div>
<h1>{data.login}</h1>
{data.name && <p>{data.name}</p>}
{data.location && <p>{data.location}</p>}
</div>
</div>
);
return <Fetch uri={`https://api.github.com/users/${login}`}renderSuccess={userDetails} />;
)
}
function useIterator(items = [], initialValue = 0) {
const [index, setIndex] = React.useState(initialValue);
const prev = React.useCallback(() => {
if (index === 0) return setIndex(items.length - 1);
setIndex(index - 1);
}, [index]);
const next = React.useCallback(() => {
if (index === items.length - 1ndex) return setIndex(0);
setIndex(index + 1);
}, [index]);
const item = React.useMemo(() => items[index], [index]);
return [item || items[0], prev, next];
}
function RepoMenu({ repositories, onSelct = f => f}) {
const [{ name }, previous, next] = useIterator(repositories);
React.useEffect(() => { if (name) onSelect(name); }, [name]);
return (
<div style={{ display: 'flex' }}>
<button onClick={previous}><</button>
<p>{name}</p>
<button onClick={next}>></button>
</div>
);
}
function UserRepositories({ login, selectedRepo, onSelect = f => f}) {
const renderSuccess = ({ data }) => <RepoMenu repositories={data} selectedRepo={selectedRepo} onSelect={onSelect} />;
return <Fetch uri={`https://api.github.com/users/${login}/repos`} renderSuccess={renderSuccess} />;
}
8.4 Introducing GraphQL
GraphQL を扱うライブラリはたくさんあるが、ここでは graphql-request
を使う。
npm i graphql-request
import { GraphQLClient } from 'graphql-request';
const query = `
query findRepos($login:String!) {
user(login: $login) {
login
name
location
repositories(first:100) {
totalCount
nodes {
name
}
}
}
}
`;
const client = new GraphQLClient('https://api.github.com/graphql', { headers: { Authorization: 'Bearer <PERSONAL_ACCESS_TOKEN>' } });
client
.request(query, { login: 'moontahoe' })
.then(results => JSON.stringify(results, null, 2))
.then(console.log)
.actch(console.error);
レスポンス:
{
"data": {
"user": {
"login": "MoonTahoe",
"name": "Alex Banks",
"location": "Tahoe City, CA",
"repositories": {
"totalCount": 51,
"nodes": [
{
"name": "snowtooth"
}
// ...
]
}
}
}
}
9 Suspense
9.1 Error Boundaries
Ref: https://reactjs.org/docs/error-boundaries.html
9.2 Code Splitting
Ref: https://reactjs.org/docs/code-splitting.html
9.3 Introducing: The Suspense Componennt
Ref: https://reactjs.org/docs/concurrent-mode-suspense.html
10 React Testing
10.1 ESLint
ESLint は JavaScript 用の最新のコードリンタである。
npm install eslint --save-dev
npx eslint --init
npx eslint .
10.2 ESLint Plug-Ins
.eslintrc.json
:
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
10.3 Prettier
npm install -g prettier
npm install eslint-config-prettier eslint-plugin--prettier --save-dev
# {
# "extends": ["plugin:prettier/recommended"],
# "plugins": ["prettier"],
# "rules": ["prettier/prettier": "error"]
# }
npx prettier --write "src/*.js"
10.4 Typechecking for React Applications
npx create-react-app my-type --template typescript
10.5 Test-Driven Development
Star.test.js
:
import Star from './Star';
describe('Star', () => {
test('renders a star', () => {
const div = document.createElement('div');
ReactDOM.render(<Star />, div);
expect(div.querySelector('svg')).toHaveAttribute('id', 'hotdog');
});
});
Checkbox.test.js
:
import { render } from '@testing-librarry/react';
import Checkbox from './Checkbox';
describe('Checkbox', () => {
test('Selecting the checkbox should toggle its value', () => {
const { getByLabelText } = render(<Checkbox />);
const checkbox = getByLabelText(/not checked/i);
fireEvent.click(checkbox);
expect(checkbox.checked).toEqual(true);
fireEvent.Click(checkbox);
expect(checkbox.checked).toEqual(false);
});
});
10.6 Using Code Coverage
npm test -- --coverage
11 React Router
Angular, Ember, Backbone と違って、React は標準ルータを持っていない。React Router ライブラリが作られ、広く使われている。
pages.js
:
import React from 'react';
import { Link, useLocation, useParams } from 'react-router-dom';
export function Home() {
return (
<div>
<h1>[Compoany Website]</h1>
<nav>
<Liink to='about'>About</Link>
<Liink to='events'>Events</Link>
<Liink to='products'>Products</Link>
<Liink to='contact'>Contact Us</Link>
</nav>
</div>
);
}
export function About() {
return <div><h1>[About]</h1><Outlet /></div>;
}
export function Events() {
return <div><h1>[Events]</h1></div>;
}
export function Products() {
return <div><h1>[Products]</h1></div>;
}
export function Product() {
let { id } = useParams();
return <div><h1>[Product {id}]</h1></div>;
}
export function Contact() {
return <div><h1>[Contact]</h1></div>;
}
export function Whoops404() {
return <div><h1>Resource not found at {useLocation().pathname}</h1></div>;
}
export function Services() {
return (
<section>
<h2>Our Services</h2>
<p>...</p>
</section>
);
}
<Outlet />
が使われているけど、2021/01/09 段階ではまだベータバージョンの v6 の機能だった。また v6 自体がなんかしばらく更新がない。
App.js
:
import React from 'react';
import { Routes, Route } from 'react-router-dom';
import { Home, About, Events, Products, Contact, Whoops404, Services } from './pages';
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />}>
<Route path="services" element={<Services />} />
</Route>
<Route path="/events" element={<Events />} />
<Route path="/products" element={<Products />} />
<Route path="/products/:id" element={<Products />} />
<Route path="/contact" element={<Contact />} />
<Route path="*" element={<WHoops404 />} />
</Routes>
);
}
リダイレクトは Redirect
コンポーネントを使う:
<Routes>
<Redirect from="services" to="about/services" />
</Routes>
12 React and the Server
イソモーフィック(isomorphic) と ユニバーサル(universal) は、クライアントとサーバの両方で動くアプリケーションを意味する。イソモーフィックなアプリケーションは、複数のプラットフォームでレンダリングできること。ユニバーサルなアプリケーションは、複数の環境で同じコードで動くことを意味する。
12.4 Server Rendering with Next.js
mkdir project-next
cd project-next
npm init -y
npm install --save react react-dom next
mkdir pages
pages/Header.js
:
import Link from 'next/link';
export default function Header() {
return (
<div>
<Link href="/"><a>Home</a></Link>
<Link href="/pets"><a>Pets</a></Link>
</div>
)
}
pages/index.js
:
import Header from './Header';
export default function Index() {
return (
<div>
<Header />
<h1>Hello everyone!</h1>
</div>
)
}