"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() を使う。

  1. レンダリングする。
  2. useLayoutEffect が呼ばれる。
  3. ブラウザが描画する。コンポーネント要素が実際に DOM に追加されるとき。
  4. 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}>&lt;</button>
      <p>{name}</p>
      <button onClick={next}>&gt;</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>
  )
}
Created at