Reactハンズオンラーニング読む
書籍
書籍のGitHub
モチベーション
Reactは実作業ベースで公式サイトと睨めっこしたり都度ぐぐったりしているが、Next.jsの登場もありフロントエンド構築のライブラリとしてはほぼ一強状態と言える。今後もエコシステムがReactを中心に回って行くことが予想できるため、体系的に読んでおきたいと思った。
構成
- 1章 Reactの世界へようこそ
- 2章 React学習に必要なJavaScript の知識
- 3章 JavaScript における関数型プログラミング
- 4章 Reactの基本
- 5章 React とJSX
- 6章 ステート管理
- 7章 フック
- 8章 データ
- 9章 サスペンス
- 10章 test
- 11章 ルーティング
- 12章 サーバーサイド React
感想
フック、サスペンスまで含めて体系的に再確認できたのはよかった。ただ、Next.jsが幅を利かせている昨今、もう少しサーバーサイドレンダリングについて深堀りしてほしかった。
- SSRのときのキャッシュ管理
-
getServerSideProps
でも render でも データフェッチしているときの挙動 どうなるのこれ - データフェッチライブラリがそのあたりをどう解決しようとしているのか
- SSRにおけるcss、hydrate時のcss
React の話と Next.js の話の境界が難しいかな。でもそれがそのまま僕ののうみそを攻撃してくる要因なのでスッキリしたい。
1章 Reactの世界へようこそ
2017年 React Fiber による内部アルゴリズムの刷新
これがわかりやすかった。絶対的なパフォーマンス向上というよりは、UX向上のための修正のようだ。
2019年 React Hooks 導入
「なぜ」必要だったかはこれは公式サイトがわかりやすいと思う。
React にはステートフルなロジックを共有するためのよりよい基本機能が必要
これにつきそう。要するに、データ(状態)と振る舞い(事前処理、事後処理、レンダリング)を持たせられるんだからReactコンポーネントはクラス構造で持たせればいいじゃんという考えだったし実際そうしたが、アプリケーションやエコシステムの発展につれて「ステートフルな振る舞いを切り出して共有したい」という需要が出てきたと。
僕自身はちょうど Hooks が導入され始めた頃からReactを触り始めて、みんなのざわつきを見てHooksに全BETすることにしたのであまりクラスコンポーネント特有の辛みなどは経験していない。いろんなところで使い回す状態付きの振舞いを共有しようと思ったら、クラスコンポーネントでどうやるんだろう...?空のレンダリングを行うクラスコンポーネントを使ってライフサイクルフックだけ使うとか?あまり考えたくない。
いずれにしても今はそういった色々な先人の努力の上でありがたく洗練されたReactを使わせてもらっている。
2章 React学習に必要な JavaScript の知識
知らなかったところや、改めて勉強になったところをピックアップ。
var と let のスコープ
varを明示的に使うことがこれまでなかったので知らなかった。
var topic = "JavaScript"
if(topic){
var topic = "React"
console.log("block",topic)
}
console.log(topic) // React
var topic2 = "JavaScript"
if(topic2){
let topic2 = "React"
console.log("block",topic2)
}
console.log(topic2) // JavaScript
var
だと宣言し直しても名前空間を共有しているので、上書きのような形になってしまうようだ。バグの温床でしかなさそうなので、少なくともアプリケーションコードでは使わない方が良さそう。
オブジェクトと配列のデストラクチャリング
これ名前が分からなくて「extract assign」とかで検索してた。デストラクチャリングっていうのね。それで、これのパターンの時に、「配列の2番目の値は使いたいけど1番目は捨ててOK」って時にどう書けばいいか分からなかった。
const [_, second] = ["wada", "igarashi"]
console.log(second)
これだとIDEとかで警告が出ちゃうんですよね。
const [, second] = ["wada", "igarashi"]
console.log(second)
何も書かなくてもいいのかー。
randomeuser.me
非同期処理のところで出てきたこのランダムユーザーを返すAPI。こんな便利なものが!知りませんでした。
fetch("https://api.randomuser.me/?nat=US&results=1")
3章 JavaScript における関数型プログラミング
React Hooks のアプローチの意図を知ろうということでしょうか。アプリケーションを構築するところをコピペして実行してみます。
// デジタル時計(関数型プログラミング的アプローチ)
const oneSecond = () => 1000
const getCurrentTime = () => new Date()
const clear = () => console.clear()
const log = message => console.log(message)
const abstractClockTime = date =>
({
hours: date.getHours(),
minutes: date.getMinutes(),
seconds: date.getSeconds()
})
const civilianHours = clockTime =>
({
...clockTime,
hours: (clockTime.hours > 12) ?
clockTime.hours - 12 :
clockTime.hours
})
const appendAMPM = clockTime =>
({
...clockTime,
ampm: (clockTime.hours >= 12) ? "PM" : "AM"
})
const display = target => time => target(time)
const formatClock = format =>
time =>
format.replace("hh", time.hours)
.replace("mm", time.minutes)
.replace("ss", time.seconds)
.replace("tt", time.ampm)
const prependZero = key => clockTime =>
({
...clockTime,
[key]: (clockTime[key] < 10) ?
"0" + clockTime[key] :
clockTime[key]
})
const compose = (...fns) =>
(arg) =>
fns.reduce(
(composed, f) => f(composed),
arg
)
const convertToCivilianTime = clockTime =>
compose(
appendAMPM,
civilianHours
)(clockTime)
const doubleDigits = civilianTime =>
compose(
prependZero("hours"),
prependZero("minutes"),
prependZero("seconds")
)(civilianTime)
const startTicking = () =>
setInterval(
compose(
clear,
getCurrentTime,
abstractClockTime,
convertToCivilianTime,
doubleDigits,
formatClock("hh:mm:ss tt"),
display(log)
),
oneSecond()
)
startTicking()
たしかに、1秒ごとにコンソールがクリアされ、秒が増えていく様子が確認できました。こんな書き方もできるんですね。
4章 React の基本
配列からコンポーネントを作る部分とかはもうやってるので飛ばしました。
React コンポーネントの歴史
第一期:createClass
2013年当時のコンポーネント作成方法だそうです。なんだこれ!?
const IngredientsList = React.createClass({
displayName: "IngredientsList",
render() {
return React.createElement(
"ui",
{className: "ingredients"},
this.props.items.map((ingredient, i) =>
React.createElemetn("li", {key: i}, ingredient)
)
)};
});
この React はあまり使いたくないですね… class
構文がまだなかったのでしょうか。
第二期:createClass
例の class コンポーネントですね。やはりES2015でclassキーワードが導入されて、React はいちはやく導入したとかいてます。すごいですね。
5章 React と JSX
4章の、APIを使ってElementを定義するやり方は見通しがかなり悪いということで、JSXの登場です。わたしは最初、JSの記述の中にHTML的なやつが return されててなんじゃこりゃ!?と思ったものです。コンポーネント単位の Building Block と認識すれば、さほど難しいことはありません。
webpack
私は create-react-app や Next.js を使ったことがほとんどなので、webpack を意識した経験が少ないです。唯一、ふたつのReact Appの共通コンポーネントを作ろうとしたときに、eject
して webpack をいじるかどうか検討したことがあります。結局自分の手に負えなそうだったのでその方法は採用しませんでした。
Next.js を使う場合も、バンドルの軽量化は Dynamic Import でまかなうことが多いのではないでしょうか。とはいえ中身についてしっておくことは後々得になります。webpack でどんなことができるかだけメモしておきます。
あとは Create React App が何をやっているかという話でした。フォルダ構成、import文の処理、ビルド時の動き、bundle.js の生成、ソースマップの定義、です。
6章 ステート管理
ここでは星のアイコンでレーティングを行えるコンポーネントを例に useState hooks を利用していました。このチャプターでは極端な例としてあらゆる素材をpropsで受け取ってそれを使い描画するサンプルがあります。
- データがprops経由でコンポーネントツリーを上から下に伝わる
- ユーザーの操作は関数props経由でコンポーネントツリーを下から上に伝わる
という言語化はたしかにそうだなと思いました。
私はこれまで何をpropsで渡して何をコンポーネント内で定義意するかについて、なんとなくで対処してきました。このあたりってどうなんでしょう。基本的には Page側で必要なhooksを呼び出し、propsで各コンポーネントに流してやるのが良いと思います。でも、子コンポーネントonlyで完結するプロパティまで外から渡してあげて純粋関数にしてあげるべきなのでしょうか。正直まだわからないのでここは探り探りやっていきます。基本的には外部とのやりとりを伴わずに「状態の変化」を表現したいときにstateを使えばいいのかな?
これもしかしてサーバーサイドでいうところの関数をcombineして一連の処理を組み立てるvs状態をもつクラスコンポーネントをシーケンス順に呼び出すみたいな話?
6.3 フォーム
書籍では制御されたコンポーネントの例も紹介されていますが、react-hook-form でしばらく頑張ろうかなと思ってます。
react-hook-form はrefを使っているので、値のリフレッシュやバリデーションのトリガーなどは react-hook-form 側で用意された hooks を使うことになります。要するにライブラリへの慣れが必要です。
6.4.4 コンテキストとカスタムフックの併用
いろんなところから useContext(ColorContext)
みたいに呼び出しまくってたけど、たしかに下のようにひとつかぶせると便利かも。
import React, { createContext, useState, useContext } from "react";
import colorData from "./color-data.json";
import { v4 } from "uuid";
const ColorContext = createContext();
export const useColors = () => useContext(ColorContext);
export default function ColorProvider({ children }) {
const [colors, setColors] = useState(colorData);
const addColor = (title, color) =>
setColors([
...colors,
{
id: 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>
);
}
使う側はこう。
import React from "react";
import StarRating from "./StarRating";
import { FaTrash } from "react-icons/fa";
import { useColors } from "./ColorProvider";
export default function Color({ id, title, color, rating }) {
const { rateColor, removeColor } = useColors();
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>
);
}
const { rateColor, removeColor } = useColors();
という感じで使えるので useContext
よりも直感的でカスタムフックっぽく使えますね。
7章 フック
- useEffect
- useLayoutEffect
- useReducer
- useCallback
- useMemo
について詳しくみていくもよう。あんまりわかっていないのでありがたい。
7.1 useEffect
useEffect はコールバック関数を引数にとり、関数はコンポーネントの描画が完了したあとに呼びだされる。「描画の一部ではない」処理という意味でこの関数は副作用になる。alert や console.log などのAPI呼び出しは useEffect内で行うのがよい。
また、確実に描画が完了するのを待ってから実行したい処理を記述するためにも利用できる。以下の例ではコンポーネントが描画されたあとに、テキスト入力欄にフォーカスをあてる。
useEffect(() => {
txtInputRef.current.focus();
})
基本的にReactのアプリケーションでは、データが更新され、コンポーネントが再描画され、副作用が実行される、という一連の処理サイクルが繰り返し実行されている。
感想:便利そうだけど、多用するとコードの見通しが悪くなりそう。
7.1.1 依存配列
整理だけ。
- 配列(useEffectの第2引数)を指定しない:描画がおわるごと
- 空の配列を指定:コンポーネントの初回描画時のみ
- 配列に依存する値を指定:その値の変更による再描画がおわるごと
番外:初回は実行されず、依存する値による再描画でのみ実行される副作用の書き方👇
7.1.2 依存配列の同一性チェック
useMemo
の話である。useEffect
の依存配列が例えばコンポーネントのpropsから新しく生成しなければならない配列の場合、同一性チェックで中身が同じだとしても常に異なるインスタンスと判定されてしまう課題があるらしい。
import React, { useState, useEffect } from "react";
const useAnyKeyToRender = () => {
const [, forceRender] = useState();
useEffect(() => {
window.addEventListener("keydown", forceRender);
return () => window.removeEventListener("keydown", forceRender);
}, []);
};
function WordCount({ children = "" }) {
useAnyKeyToRender();
const words = children.split(" ");
useEffect(() => {
console.log("fresh render");
}, [words]);
return (
<>
<p>{children}</p>
<p>
<strong>{words.length} - words</strong>
</p>
</>
);
}
export default function App() {
return <WordCount>sick power day</WordCount>;
}
抜粋
上の変数 words は配列のリテラル値で初期化されているので、コンポーネントが描画されるたびに配列のインスタンスが生成されてしまいます。したがって、配列の内容は
["sick", "power", "day"]
のまま変わっていないのdに、JavaScript ではそれらを異なる配列とみなします。結果的に、描画のたびに「fresh render」の文字がコンソールに出力されてしまいます。
実際にやってみた結果。
キーを押すと空ステートが更新されて強制再描画みたいな動きになっていて、依存配列のワードリスト自体は変わってないけど、描画のたびに console.log が動いているので 同じ依存配列でも違うインスタンスとして判定されているってことになる。
useMemo
こういうケースで useMemo
をつかうらしい。配列 words を作成するのに useMemo
を使うと。ふーん。これまでコンポーネントに対して useMemo
みたいにしているコードしかみたことなかった。小さい単位では値に対しても使うのね。
const words = useMemo(() => {
children.split(" ")
}, [children]);
useEffect(() => {
console.log("fresh render");
}, [words]);
これで children プロパティが変化しない限り、String.split は呼び出されず、代わりにキャッシュされた値がwordsに代入されるため、 words の参照する配列のインスタンスは同一であることが保証される。useState
で配列を管理して、useEffect
でchildrenに変化があったときにstateを更新するように定義するのと同じ結果になるかしら?
useCallback
useMemo
がメモ化された値を返すのに対し、useCallback はメモ化された関数を返す。
const fn = useCallback(() => {
console.log("hello");
console.log("world");
}, [ ]);
useEffect(() => {
console.log("fresh render");
fn();
}, [fn]);
もし useCallback
でない普通の 関数として fn
を定義していたら、描画のたびに fn
は新しいインスタンスとなり fresh render
が出力される。上記の例のように useCallback
を使っていれば依存配列が空なので初回だけ実行され、意図どおり初回のみ fresh render
がコンソール出力される。
7.2 useLayoutEffect
こういう順番になるらしい。
- コンポーネントの描画関数が呼び出される
- useLayoutEffect で設定した副作用関数が呼び出される
- ブラウザの Paint 処理によるコンポーネントの描画結果が画面に反映される
- useEffect で設定した副作用関数が呼び出される
例えば描画前にブラウザウィンドウサイズをもとにコンポーネントのサイズを計算するとか。ちらつきを防ぐとかもこのあたりでやるといいかもしれないですね。
7.2.1 フックの使い方に関するルール
- フックはコンポーネントのスコープで実行すること
- ひとつのフックで多くのことをせず複数のフックに分割すること
- フックは常に描画関数のトップレベルから呼び出さなければならない
- フックを非同期呼び出しすることはできないが、副作用関数の内部であれば非同期処理を呼び出すことが可能
4はこういう感じ
useEffect(() => {
const fn = async () => {
await callApi();
};
fn();
});
10章 テスト
- ESLint による静的解析
- Prettir によるフォーマット
- 型チェック(私は主にTypeScriptで)
- Jest
このあたりは把握。
10.6 React コンポーネントのテスト
コンポーネントのテストで必ずしもブラウザは必要なく、Node.js を使ってテストすることが可能です
へぇ〜そうなんだ。でもマウント後の useEffect()
はテストできなくない…?そういうのは含んでないんだろか。
import React from "react";
import ReactDOM from "react-dom";
import Star from "./Star";
import { toHaveAttribute } from "@testing-library/jest-dom";
import { render } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
expect.extend({ toHaveAttribute });
test("renders a star", () => {
const div = document.createElement("div");
ReactDOM.render(<Star />, div);
expect(div.querySelector("svg")).toHaveAttribute(
"id",
"star"
);
});
こんな感じで <div/>
にレンダリングして結果をテストできる。なお、マッチャの充実度などから React Testing Library を使うといいらしい。
10.6.3
なるほどこの React Testing Library を使えば イベントのテストもできるらしい。
import React from "react";
import { render, fireEvent } from "@testing-library/react";
import { Checkbox } from "./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);
});
12章 サーバーサイドReact
12.1 アイソモーフィックとユニバーサル
- アイソモーフィック:複数のプラットフォームでレンダリング可能なアプリケーション
- ユニバーサル:コードを書き換えることなく複数のプラットフォーム(ブラウザ、nodejs)で実行可能なアプリケーション
うーん、違いがわからない。とりあえず先に進む。
=> なんとなくわかった。
// これがアイソモーフィック
if(env = 'browser'){
ReactDOM.render(<Start />);
else {
ReactDOMServer.renderToString(<Star / >);
}
// これがユニバーサル
console.log('めんどくさくて省略したけど同じコードが実行できる的な意味合いが強そう');
12.2 React におけるサーバーサイドレンダリング
SSRした場合、ReactDOM.render
のかわりにReactDOM.hydrate
を使うことでReactDOMServerにより描画されたコンポーネントをブラウザで再利用できる。これを最大限活用しているのが Nest.js
だよねって思ったら次で言及されてた
12.3 Next.js
書籍の内容がもはや古いので、Next.js の進化スピードやべえなって思いました。
12.4 Gatsby
一時期流行ったけど、いまはどういう感じで使われているんだろうか。
12.5 React の未来
なにか作ろうという話でしたね。ありがとございました。