概要
React(Redux)のstate更新が想定通りにいかずに半べそかいてる過去の自分に向けた手紙
解決できること
「stateが更新されないToT」という状態から脱出できる。
version
背景
開発メンバーから「useSelectorで取得してる値が古いままなんですが…」と相談を受けました。
そして、思い出しました。自分もReact触り始めの時に同じ症状で苦しんだことを。
自分もちゃんと原因確かめないまま進んでいたなと思い出して、備忘録も兼ねて書いておきます。
Redux Toolkitをsampleに進めていきます。
やりたいこと
-
関数A
というbutton押下でAの値が+5される。押した回数増えていく -
関数B
というbutton押下でBの値にAの値を加える
(例) 関数B
を実行すると
state \ 回数 | 1 | 2 | 3 | 4 |
---|---|---|---|---|
A | 5 | 10 | 15 | 20 |
B | 5 | 15 | 30 | 50 |
という振る舞いを実装したい。
元にしたcode sandbox
製作者に感謝m_ _m
ディレクトリ構造とかは今回簡易的な試すだけなのでご了承下さい。
①Aのbuttonを実装する
Redux(Toolkit)知ってるよって人は飛ばしてください。
-
hogeSlice
というfileにreducerやら、stateが格納されていてexportしてます - stateを使うときは
useSelector
で参照する - exportした関数は
dispatch(hoge(payload))
で実行する - Reduxと比べて非同期処理がシンプルに書ける
この辺りが特徴かと思います。
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { plusA } from "./counterSlice";
import styles from "./Counter.module.css";
export function Counter() {
const count = useSelector((state) => state.counter);
const dispatch = useDispatch();
return (
<div>
<div className={styles.row}>
<button
className={styles.button}
onClick={() => dispatch(plusA())}
>
関数A
</button>
Aの値: {count.valueA}
</div>
</div>
);
}
import { createSlice } from "@reduxjs/toolkit";
export const slice = createSlice({
name: "counter",
initialState: {
valueA: 0
},
reducers: {
plusA: (state) => {
state.valueA += 5;
}
}
});
export const { plusA } = slice.actions;
export default slice.reducer;
②Bのbuttonを実装する
はい。不具合が出てますね。
みた感じはAの更新される前の値がBに加えられているように見えます。
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { plusA, plusB } from "./counterSlice";
import styles from "./Counter.module.css";
export function Counter() {
const count = useSelector((state) => state.counter);
const dispatch = useDispatch();
const addState = () => {
dispatch(plusA());
dispatch(plusB(count.valueA));
};
return (
<div>
<div className={styles.row}>
<button
className={styles.button}
onClick={() => dispatch(plusA())}
>
関数A
</button>
Aの値: {count.valueA}
</div>
<div className={styles.row}>
<button
className={styles.button}
onClick={() => addState()}
>
関数B
</button>
Bの値: {count.valueB}
</div>
</div>
);
}
import { createSlice } from "@reduxjs/toolkit";
export const slice = createSlice({
name: "counter",
initialState: {
valueA: 0,
valueB: 0
},
reducers: {
plusA: (state) => {
state.valueA += 5;
},
plusB: (state, action) => {
state.valueB += action.payload;
}
}
});
export const { plusA, plusB } = slice.actions;
export default slice.reducer;
state \ 回数 | 1 | 2 | 3 | 4 |
---|---|---|---|---|
A | 5 | 10 | 15 | 20 |
B | 5 | 15 | 30 | 50 |
ではなく、
state \ 回数 | 1 | 2 | 3 | 4 |
---|---|---|---|---|
A | 5 | 10 | 15 | 20 |
B | 0 | 5 | 15 | 35 |
となっています。
試したこと
- 拡張機能を使ってstateを追いかける
- たくさんconsole.log(state)して中身を見にいく
- useSelectorの公式を読む
- 非同期的なことかと思い、dispatchにawaitつけたり、reducerの処理を非同期にしてみる
この辺りを試していくうちに「よくわからん」となることが多いのかなと思います。
想定通りにならない原因
useState や useReducer と組み合わせる
useStateやuseReducerによる更新とdispatchによる更新を、一度のイベントのなかで同期的に行った場合、再レンダリングは一度だけ行われる。
React書いてると忘れがちになるのですが、そもそもcomponentって関数なので、内包されてる値は実行時に決まります = レンダリングのタイミング。
この辺の感覚を再認識して修正していきます。
改修方法① store.getState()を使う
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { plusA, plusB } from "./counterSlice";
import styles from "./Counter.module.css";
import store from "../../app/store";
export function Counter() {
const count = useSelector((state) => state.counter);
const dispatch = useDispatch();
const addState = () => {
dispatch(plusA());
const res = store.getState();
dispatch(plusB(res.counter.valueA));
};
return (
<div>
<div className={styles.row}>
<button
className={styles.button}
onClick={() => dispatch(plusA())}
>
関数A
</button>
Aの値: {count.valueA}
</div>
<div className={styles.row}>
<button
className={styles.button}
onClick={() => addState()}
>
関数B
</button>
Bの値: {count.valueB}
</div>
</div>
);
}
const res = store.getState();
dispatch(plusB(res.counter.valueA));
一番最初に思いついた方法。
ただ感覚的にはあまりイケてない気がする。もうちょっと他の人のコード見てみたいなぁ…
storeを至るところで呼ぶというのがあまり良くない気がする(気がするだけかもしれない)
改修方法② reducerのロジックを変更する
import { createSlice } from "@reduxjs/toolkit";
export const slice = createSlice({
name: "counter",
initialState: {
valueA: 0,
valueB: 0
},
reducers: {
plusA: (state) => {
state.valueA += 5;
},
plusB: (state) => {
state.valueB += state.valueA;
}
}
});
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { plusA, plusB } from "./counterSlice";
import styles from "./Counter.module.css";
export function Counter() {
const count = useSelector((state) => state.counter);
const dispatch = useDispatch();
const addState = () => {
dispatch(plusA());
dispatch(plusB());
};
// 以下略
①よりはまだ良いと思っています。storeの中の値は更新されているので振る舞い通りに実装できる。
ユースケース次第では冗長的になりすぎる気もする。
改修方法③ createAsyncThunkを使う
今回のケースでは関係ありませんが、実際のケースではこういった簡易なstate更新は少なく、
API叩いてデータとってきて…ということが多くなると思うので、非同期実装が必要なタイミングもあるので備忘録的な意味で書いておきます。
上記①〜③を書いたsandbox
Redux Toolkit公式
あとがき
そろそろRecoilやらSWRも触っていきたいお年頃…
参考にさせて頂いた記事