React状態管理:カスタムフック・Context・Reducerの導入スライド
React状態管理:カスタムフック・Context・Reducer - なぜカスタムフックか
同じ12行が2つ並ぶ
トップスの一覧を、くつの一覧とまったく同じ書き方で足すとどうなるか見てください。
変わるのは category のところ1か所だけです。
| 部品 | 取得コード |
|---|---|
| ShoesList | category=shoes を fetch する12行 |
| TopsList | category=tops に変えただけの同じ12行 |
1 / 5
React状態管理:カスタムフック・Context・Reducer - なぜカスタムフックか
直す場所を数える
取得の置き場を切り替えて、再試行を足したときに直す場所がいくつになるか数えてください。
- 2つに写すのままだと、直す場所が部品の数だけ増える
- 再試行を足すを入れると、同じ1行が2つの部品にそのまま増える
- useItems にまとめるへ切り替えると、直す場所は1か所のまま
2 / 5
重複が高くつく理由
写すのは一瞬でも、高くつくのはそのあとです。
失敗したときの再試行を足したくなる
2か所とも直すことになる
取得中に別カテゴリへ切り替える対策を足す
ここも2か所とも直すことになる
片方だけ直して、もう片方を直し忘れる
画面によって挙動が違う、見つけにくい不具合になる
同じロジックがn個あると、直す手間もn倍になります。
3 / 5
見た目と、ロジックの違い
同じ見た目はコンポーネントにする、が今回も通用するか確かめます。
見た目が同じ2つ
コンポーネントに分ければ重複が消える
ロジックだけ同じ2つ
見た目は別。コンポーネントでは切り出せない
そっくりなのは見た目ではなく、state と useEffect の組み合わせのほうです。
4 / 5
考えてみよう外しても進めます
同じ取得ロジックが2つの部品にあります。コンポーネントに分けても解決しない理由はどれですか。
選ぶと、この枚の中で答えと理由を確かめられます。
5 / 5
12行が呼び出し1行に
取得のコードを直接書いた場合と、useItems を呼ぶだけにした場合を比べてください。
ShoesList に直接書く
useEffect(() => {
fetch("/api/items?category=shoes")
.then((r) => r.json())
.then((d) => setItems(d));
}, []);useItemsを呼ぶだけ
const { items, loading } =
useItems("shoes");12行あった取得の手続きが、呼び出し1行に変わります。
1 / 5
React状態管理:カスタムフック・Context・Reducer - useItemsを作る
返ってくる中身を見る
いまの状態を切り替えて、カスタムフックが返すオブジェクトの中身がどう変わるか確かめてください。
- 届いたにすると、items に3件入り、loading が false になる
- 失敗したで error も返すを外すと、0件のときと見分けが付かなくなる
- 使う側は分割代入で受け取るだけでよい
2 / 5
よくある勘違い
よくある勘違いを先に潰しておきます。
fetchItems のように use を付けずに名前を付ける
中でフックを呼んでいることに検査の道具が気付けない
useItems を2つの部品から呼ぶ
items が混ざると誤解する
JSX を返す関数だと思い込む
実際は state と、それを動かす仕組みを返すだけ
共有されるのはコードであって、state ではありません。
3 / 5
呼び出しごとに独立する
useItems はただの関数です。呼んだ部品ごとに別々の items と loading が返ります。
useItems(ただの関数return { items, loading }値と道具を返す結果
| 呼び出し | items |
|---|---|
| useItems("shoes") | 3件(くつ) |
| useItems("tops") | 2件(トップス) |
4 / 5
考えてみよう外しても進めます
ShoesList と TopsList が両方 useItems を呼んでいます。一方の items が増えると、もう一方の items はどうなりますか。
選ぶと、この枚の中で答えと理由を確かめられます。
5 / 5
stateを1つにまとめる
入力欄ごとに state を分けると、欄が増えるたびにハンドラも増えます。
欄ごとにstateを分ける
const [title, setTitle] =
useState("");
const [price, setPrice] =
useState("");1つのオブジェクトにまとめる
const [values, setValues] =
useState({ title: "", price: "" });欄がいくつ増えても、まとめる state は1つのままです。
1 / 5
React状態管理:カスタムフック・Context・Reducer - useFormを作る
打った値の流れ
打っている欄を選んで文字数を増やし、打った値がどこへ流れるか見てください。
- 打っている欄を切り替えると、色が付く行と入力欄の name が一緒に動く
- 打った文字数を増やすと、values の行と入力欄の中身が同時に伸びる
- 0文字のときは、まだ何も打っていない状態が出る
2 / 5
書き換えで事故る3つ
handleChange を書くとき、ここでよくつまずきます。
...prev を広げずに上書きする
打った欄以外の値が消える
e.target.name ではなく決め打ちのキーで書き換える
他の入力欄を打ってもそのキーだけ書き換わる
reset で initial 以外の値を戻す
送信後に前回の入力が残ったままになる
[name] は変数の中身をキーにする書き方です。
3 / 5
useFormの中身
入力の書き換えを受け持つ handleChange まで書いた useForm です。
呼び出しと結果
| 打った操作 | values の中身 |
|---|---|
| title欄に赤いくつと打つ | { title: "赤いくつ", price: "" } |
| 続けてprice欄に3200と打つ | { title: "赤いくつ", price: "3200" } |
| title欄を消す | { title: "", price: "3200" } |
4 / 5
考えてみよう外しても進めます
setValues の中で ...prev を書かずに { [name]: value } とだけ返すと、何が起きますか。
選ぶと、この枚の中で答えと理由を確かめられます。
5 / 5
React状態管理:カスタムフック・Context・Reducer - フックのルール
2つのルールしかない
自分でフックを作れるようになると、置き場所を自分で決めることになります。
この2つを守れば、置き場所で困りません。
| ルール | 内容 |
|---|---|
| 呼ぶ場所 | いちばん上の階層で呼ぶ。if や for の中では呼ばない |
| 呼ぶ相手 | コンポーネントか、use で始まる名前の関数の中だけ |
1 / 5
React状態管理:カスタムフック・Context・Reducer - フックのルール
順番が入れかわる瞬間
描画の回を1回目から2回目に切り替えて、1番目の棚に何が入るか確かめてください。
- 2回目にすると、1番目の棚は別の呼び出しの値を受け取ってしまう
- if の中のフックは呼ばれない回ができ、数がそろわなくなる
- フックを if の外に出すを入れると、2回目でもそれぞれの値に戻る
2 / 5
壊れ方は静かに始まる
条件がずっと真のあいだは何事も無く動くので、気づきにくいまま壊れます。
フックより前に return を置く
その回だけ呼ぶ数が減り、同じ壊れ方が起きる
use で始まらない名前でフックを呼ぶ
検査の道具が見分けられず、警告すら出ない
条件が真のあいだだけ動かして満足する
あとで条件を変えた日に、別の場所が壊れる
条件はフックを囲むのではなく、フックの中に入れます。
3 / 5
条件はフックの中に
同じ特定のときだけ処理したいでも、書く位置で結果が変わります。
フックを囲む
if (category !== "") {
useEffect(() => {
fetch(url);
}, [category]);
}フックの中で分ける
useEffect(() => {
if (category === "") return;
fetch(url);
}, [category]);useEffect は毎回同じ回数だけ呼ばれ、中身だけが変わります。
4 / 5
考えてみよう外しても進めます
フックを呼ぶ関数の名前を use で始めなくても動くことがあります。それでも use で始める理由は何ですか。
選ぶと、この枚の中で答えと理由を確かめられます。
5 / 5
戻り値に名前を付ける
戻り値に名前が無いと、使う側は綴りを間違えても気づけません。
型を書かない
export function useItems(
category: string
): any {
return { items, loading, error };
}戻り値に名前を付ける
export function useItems(
category: string
): UseItemsResult {
return { items, loading, error };
}名前を付けると、使う側の綴り間違いをその場で止められます。
1 / 5
React状態管理:カスタムフック・Context・Reducer - フックに型を付ける
山かっこを外すと
山かっこの型を外して、返ってくる型と打ち間違いの扱いを比べてください。
- 型を渡すと、返ってくる中身の型まで決まる
- 渡さないと型が決まらず、any のままになる
- 打ち間違いを重ねると、型がある側だけその場で止まる
2 / 5
配列で返すと起きること
3つ以上を配列で返すと、順番を覚える負担が使う側に移ります。
3つの値を配列で返す
使う側は順番を覚えないと読めず、1つ足すと全部ずれる
エラーを文字だけで返す
無いときも文字が必要になり、空文字でごまかしが生まれる
3つ以上を返すならオブジェクトにし、無い状態は null で表します。
3 / 5
3つの値に型を付ける
戻り値の3つに、名前と型を付けます。
string | nullまだ無い、を型で表す戻り値の形
| 項目 | 型 |
|---|---|
| items | Item[] |
| loading | boolean |
| error | string | null |
4 / 5
考えてみよう外しても進めます
フックが3つの値を返すとき、配列とオブジェクトのどちらで返すべきですか。
選ぶと、この枚の中で答えと理由を確かめられます。
5 / 5
4段のバケツリレー
テーマを App から PriceTag まで運ぶには、途中の2つにも受け取って渡してもらう必要があります。
App
theme を dark に決める
ItemList
受け取った theme をそのまま渡す
ItemCard
受け取った theme をそのまま渡す
PriceTag
受け取った theme を価格の見た目に使う
1 / 5
React状態管理:カスタムフック・Context・Reducer - バケツリレーの限界
値が下りていく道
どこまで渡したかを動かして、値が下りていく道を追ってください。
- 運ぶ値が theme でも user でも、通る道の作りは同じ
- List が渡し忘れるを入れると、そこから下は受け取れなくなる
- 止まった先はエラーにならず、ただ何も来ないままになる
2 / 5
使わないのに型に書く
ItemList は theme を自分では使いませんが、下に渡すために型にも呼び出しにも書きます。
theme自分では使わない3 / 5
増えると起きること
動くには動きますが、段や共有したい値が増えると辛さが膨らみます。
段が5段10段と深くなる
渡し忘れた1か所を探すのに時間が溶ける
文字サイズも共有したくなる
また全部の段に足すことになる
ItemList を別の場所で使い回す
要らない theme まで渡す羽目になる
途中の部品が、テーマを運ぶだけの通り道にされています。
4 / 5
考えてみよう外しても進めます
ItemCard で theme={theme} を書き忘れると、PriceTag はどうなりますか。
選ぶと、この枚の中で答えと理由を確かめられます。
5 / 5
React状態管理:カスタムフック・Context・Reducer - Contextを作る
箱を作る2つの情報
createContext で箱を作るとき、山かっこと丸かっこの2つを書きます。
createContext<Theme>("light") の1行に、この2つが両方入っています。
| 書く場所 | 意味 |
|---|---|
| 山かっこ | 箱に入る値の型を指定する |
| 丸かっこ | Provider に包まれていない部品が受け取る既定値 |
1 / 5
React状態管理:カスタムフック・Context・Reducer - Contextを作る
Providerが囲む範囲
Provider で包む段数を動かして、どこまで dark が届くか確かめてください。
- 包む段数を増やすと、囲みの中に入った部品が dark に変わる
- 囲みの外に残った部品は、createContext に書いた既定値のままになる
- 0段にすると、誰も包まれず全部が既定値のままになる
2 / 5
Providerで包む宣言
配る範囲は、Provider で包んだ内側です。何段深くても関係ありません。
value={theme}包んだ内側に配る値3 / 5
考えてみよう外しても進めます
createContext の丸かっこに書いた light は、いつ使われますか。
選ぶと、この枚の中で答えと理由を確かめられます。
4 / 5
createContext で箱を作り、Provider で包んだ範囲に配る
取り出す useContext は次の回で扱います。この回はまだ配る宣言までです。
5 / 5
取り出す前と後
PriceTag が theme を受け取る2通りの書き方を見比べます。
propsで運ぶ
function PriceTag(
{ price, theme }: PriceTagProps
) {
return <p>{theme}</p>;
}useContextで取る
function PriceTag({ price }: Props) {
const theme =
useContext(ThemeContext);
return <p>{theme}</p>;
}props を1つ減らすだけで、途中の受け渡しが消えます。
1 / 5
React状態管理:カスタムフック・Context・Reducer - useContextで使う
上へ探しに行く
上へ探すを1段ずつ動かして、値がどこで見つかるか追ってください。
- 上へ探すを動かすたびに、間の部品を素通りして進む
- Provider に当たったところで、その value が返る
- Provider を外すと当たらず、createContext に書いた既定値が返る
2 / 5
気づきにくい落とし穴
Provider が見つからないときの動きを確かめます。
PriceTag を Provider の外側で呼ぶ
既定値が返るだけで、エラーにはならない
切り替えても価格の色が変わらない
たいてい Provider の外側で呼んでいるのが原因
止まらないぶん、原因にたどり着くのが遅れます。
3 / 5
あちこちで同じものを見たい少数のものに Context を使う
1つの画面だけで使う値は、これまでどおり props で足ります。
4 / 5
考えてみよう外しても進めます
useContext を呼んでも画面が変わらないとき、まず疑うべき原因は何ですか。
選ぶと、この枚の中で答えと理由を確かめられます。
5 / 5
配る値の中身
Provider の value に何を入れるかで、できることが変わります。
値だけ配る
value={theme}値と関数を配る
value={{ theme, toggleTheme }}読むだけの値では、切り替えるボタンが動きません。
1 / 5
React状態管理:カスタムフック・Context・Reducer - テーマの全体共有
1か所で全部が変わる
Provider に入れるテーマを切り替えて、いくつの部品が一緒に変わるか数えてください。
- dark に切り替えると、包んだ中の部品が同時に変わる
- value に theme だけを入れるをオンにすると、切り替えるほうが動かなくなる
- 書き換えるのは value の1か所だけでよい
2 / 5
自分の中は覗けない
App の中で useContext を呼ぶとどうなるか確かめます。
App が ThemeProvider を描いている
App 自身は Provider の外側にいる
画面の本体を App の中に直接書く
useContext が既定値しか返さない
画面の本体は、Provider の中に置く部品へ切り出します。
3 / 5
読む値と切り替える関数はセットで value に入れる
配る人と使う人を分けると、押しても切り替わらない不具合を防げます。
4 / 5
考えてみよう外しても進めます
ThemeProvider を描いている App 自身の中で useContext を呼ぶと、何が返りますか。
選ぶと、この枚の中で答えと理由を確かめられます。
5 / 5
居ないことを型にする
テーマは必ずどちらかの値でしたが、ユーザーはログアウトしていれば居ません。
| null居ないことを型で表す(null)既定値も null1 / 5
React状態管理:カスタムフック・Context・Reducer - ユーザー情報の共有
外に出すと何が返るか
Greeting の置き場所を Provider の外に切り替えて、返ってくる値を確かめてください。
- Provider の外にすると、useContext は既定値の null を返す
- null チェックを書くをオンにすると、落ちずに案内文を出せる
- オフのまま外に出すと、画面は止まる
2 / 5
早めに抜けて絞り込む
if で先に return すると、そのあとの行では user が null でないと分かります。
user === null先に絞り込むuser.nameここでは必ず居る3 / 5
変わる理由で箱を分ける
テーマとユーザーは変わるきっかけが違うので、箱を分けたままにします。
1つの箱にまとめる
const AppContext = createContext({
theme: "light",
user: null,
});変わるきっかけで分ける
const ThemeContext =
createContext("light");
const UserContext =
createContext<User | null>(null);まとめると、テーマを切り替えただけでユーザーを見る部品まで描き直されます。
4 / 5
考えてみよう外しても進めます
UserContext の外側で useContext を呼ぶと、何が返りますか。
選ぶと、この枚の中で答えと理由を確かめられます。
5 / 5
3つの操作を持つカート
fleama のカートに要る操作は、入れる・数量を変える・外すの3つです。
カートに入れる
同じ商品があれば数量を1つ増やす
数量を変える
その行の数量だけを1つ増やす
カートから外す
その行を配列から外す
1 / 5
同じ計算が散らばる
handleQtyUp の中にも、数量を1つ増やす同じ計算が書いてあります。
qty: line.qty + 1handleAdd にも同じ式がある2 / 5
React状態管理:カスタムフック・Context・Reducer - setStateの散らかり
2か所に散った計算
カートのスニーカーの数量を10まで動かし、+ボタン側にも上限を書くを切り替えて確かめてください。
- 数量を10にすると、カートに入れる側の上限10がまず効く
- +ボタン側にも上限を書くをオンにするまで、+の数字だけが11に伸びる
- オンにしてそろえても、同じ決まりを2か所に書いたことに変わりはない
3 / 5
動くが読みにくい
動くには動きますが、読みにくいところが3つあります。
同じ計算を handleAdd にも書く
決まりが増えるたび両方を直すことになる
更新の中身を部品の中に書く
別の画面から触るとき同じ関数を渡す羽目になる
操作が増えていく
handleAdd と handleQtyUp を目で拾わないと分からない
片方だけ直して事故る、という形で必ず表に出ます。
4 / 5
考えてみよう外しても進めます
上限10を handleAdd にだけ書いた状態で、数量10から+ボタンを押すとどうなりますか。
選ぶと、この枚の中で答えと理由を確かめられます。
5 / 5
起きたことだけを渡す
押したその場で次の値を計算するのをやめ、起きたことだけを伝える形に変えます。
その場で次の値を渡す
setQty(qty + 1)起きたことを渡す
dispatch("increase")次の値を決める仕事は、reducer という1つの関数にまとめます。
1 / 5
React状態管理:カスタムフック・Context・Reducer - useReducer
3者の関係
dispatch で送る合図といまの数量を動かして、返ってくる次の数量を確かめてください。
- dispatch で送る合図を increase・decrease・reset で切り替えられる
- いまの数量を1にして decrease を送ると、1未満に減らさない決まりが効く
- 第1引数のいまの状態と第2引数の起きたことから、次の状態だけが返る
2 / 5
switchで遷移を並べる
qtyReducer の中に、起きたことごとの次の値をすべて並べます。
qty: numberいまの状態action: QtyAction起きたこと3 / 5
React状態管理:カスタムフック・Context・Reducer - useReducer
1行に入った3つ
useReducer が返すのは、いまの状態と dispatch の組です。
| 書く場所 | 中身 |
|---|---|
| 第1引数 | reducer 本体 (qtyReducer) |
| 第2引数 | 初期値 (1) |
| 戻り値 | いまの状態と dispatch の組 |
4 / 5
考えてみよう外しても進めます
dispatch("increase") を読んだ人が、その場で分かることは何ですか。
選ぶと、この枚の中で答えと理由を確かめられます。
5 / 5
文字だけでは足りない
前レッスンの action は文字だけでしたが、カートには商品や id という材料が要ります。
文字だけの action
dispatch("increase");書類にした action
dispatch({ type: "ADD", item });type で何が起きたかを書き、必要な材料を一緒に載せます。
1 / 5
3つのdispatchの形
ADD には商品そのもの、REMOVE と CHANGE_QTY には id が要ります。
item });ADD は商品ごと id: "1" });id だけで足りる2 / 5
React状態管理:カスタムフック・Context・Reducer - Actionのunion型
型の口が塞ぐ場所
dispatch に渡す action を切り替えて、型がどこで止めるか確かめてください。
- id を忘れた REMOVE は、dispatch に渡す前で止まる
- 無い type を送っても、渡す前で止まる
- type を string にすると検査が消え、カートまで素通りする
3 / 5
switchで絞り込まれる
action.type を switch にかけると、case の中でその形だけが読めます。
action.itemADD の中だけ読めるaction.idREMOVE の中だけ読める4 / 5
考えてみよう外しても進めます
action の type を string にすると何が起きますか。
選ぶと、この枚の中で答えと理由を確かめられます。
5 / 5
抜けは2つある
同じ商品を2回入れると行が2つ並び、数量を減らしても0のままの行が残ります。
同じ商品をもう一度 ADD する
行が2つ並んでしまう
CHANGE_QTY で0以下にする
行が0点のまま残る
1 / 5
見つけてから足す
ADD は、既に入っている行を探してから、増やすか足すかを決めます。
2 / 5
React状態管理:カスタムフック・Context・Reducer - カートreducer
行と合計が一緒に動く
dispatch を1つずつ送って、行と合計がどう動くか見てください。
- 決まりごと入りだと、同じ商品を足しても行は増えない
- そのまま足すだけに切り替えると、同じ商品が2行に並ぶ
- 数量を0にすると、行ごと外れるか残るかが変わる
3 / 5
同じ配列を返すと止まる
cart.push で書き換えたあと同じ配列を返すと、React は変わっていないと見なします。
元の配列を書き換える
cart.push(line);
return cart;新しい配列を返す
return [...cart, line];左は画面が更新されないのに、コンソールにも何も出ません。
4 / 5
考えてみよう外しても進めます
cart.push で行を足して cart をそのまま返すと、画面はどうなりますか。
選ぶと、この枚の中で答えと理由を確かめられます。
5 / 5
React状態管理:カスタムフック・Context・Reducer - 使い分け
見分ける目安
useReducer を覚えると何でも寄せたくなりますが、目安があります。
fleama では検索欄が useState、カートが useReducer 向きです。
| 向き | 目安 |
|---|---|
| useStateのまま | 値は1つ。置き換えるだけで、前の値を見なくてよい |
| useReducerへ | 1つの操作で複数の値が変わる。前の中身に更新が依る |
1 / 5
React状態管理:カスタムフック・Context・Reducer - 使い分け
書く行数で比べる
更新する場所を増やして、書く行数がどちらで増えるか見てください。
- 更新する場所を1か所に戻すと、useState のほうが短くなる
- 更新の中身を検索欄の文字にすると、決まりごとが1行で済む
- カートのまま場所を増やすと、reducer に1回書くだけで足りる
2 / 5
この更新の決まりごとを、他の人に説明する必要があるか
説明が要るなら、それは reducer に書き出せる決まりごとです。
3 / 5
混ぜすぎに注意
reducer に寄せすぎると、かえって読みにくくなることがあります。
検索欄の1行の更新まで reducer にする
action の型を足す手間が増えるだけで、読みやすくならない
カートと検索欄を1つの大きな reducer にまとめる
関係の無い値まで同じ action で触ることになる
カートは reducer、検索欄は useState のまま。混ぜてよいのが結論です。
4 / 5
考えてみよう外しても進めます
検索欄に打った文字を持つ state は、useState と useReducer のどちらに向きますか。
選ぶと、この枚の中で答えと理由を確かめられます。
5 / 5
