Redux-Toolkitで「A non-serializable value was detected」エラーが出たときの対処方法
1. 事件発生
勉強でGithub issueを取得するAPIとredux-toolkitを使ってissue検索サイトを作ってました。
export const fetchIssueList = createAsyncThunk<Issue[], InputValues, ThunkAPI>(
'issue/fetchIssueList',
async (getIssueListParam, { rejectWithValue }) => {
try {
const { data } = await getIssueList(getIssueListParam);
return formatIssueList(data);
} catch (error) {
return rejectWithValue(error);
}
});
useEffect(() => {
dispatch(fetchIssueList({ owner, repository, page: currenPageNumber }));
}, [owner, repository, currenPageNumber, dispatch]);
そして検索結果が0件のとき(=404エラーが返ってきたとき)、事件は起こったのです。
A non-serializable value was detected in an action, in the path: `payload`. Value: Error: Request failed with status code 404
at createError (createError.js:16:1)
at settle (settle.js:17:1)
at XMLHttpRequest.onloadend (xhr.js:66:1)
Take a look at the logic that dispatched this action: {type: 'issue/fetchIssueList/rejected', payload: Error: Request failed with status code 404
at createError (http://localhost:3000/static/js/bund…, meta: {…}, error: {…}}
「action の payload に non-serializable value が探知された」というエラーですが最初どういう意味かよくわからず、Redux Style Guideを探ってみました。
Avoid putting non-serializable values such as Promises, Symbols, Maps/Sets, functions, or class instances into the Redux store state or dispatched actions. This ensures that capabilities such as debugging via the Redux DevTools will work as expected. It also ensures that the UI will update as expected.
store の state、dispatch する action に non-serializable values を入れることを避けてくださいと書いてます。
でもそんなもん入れてねぇぞ( ಠ ಠ )?? と悔しい気持ちになって原因を調べました。
2. 原因分析
このエラーはfetchIssueList
がfulfilled
のときは起きずrejected
のときだけ起きたのでまずその2つの action をコンソールで出力してみました。
fulfilled action
rejected action
たしかにpayload
値の色から違いますね。rejected
のaction.payload
も出力してみます。
action.payload
正直ここで大体わかった気にもなりますが一応createError
の中身を見てみます。
なるほど。 new Error
で生成したインスタンスが payload
に入ってたからそういうエラーが出たという流れです。
3. 解決方法
3-1. serializableCheckを外す
探した1つ目解決案はserializableCheck
自体を外すことです。redux-toolkit公式サイトを参考にして書いてみました。
const store = configureStore({
reducer: rootReducer,
+ middleware: (getDefaultMiddleware) =>
+ getDefaultMiddleware({
+ serializableCheck: false,
+ }),
})
でもこれだとすべての state と action に対してserializableCheck
を外すことになるので、本当に必要な部分だけ外したい気持ちがあります。ということでスコープを絞ります。
やり方はredux公式サイトに載ってます。
configureStore({
//...
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
// Ignore these action types
ignoredActions: ['your/action/type'],
// Ignore these field paths in all actions
ignoredActionPaths: ['meta.arg', 'payload.timestamp'],
// Ignore these paths in the state
ignoredPaths: ['items.dates'],
},
}),
})
action type を指定する方法、action のパスを指定する方法、state のパスを指定する方法の3つがあります。
私の場合 state は絡んでないので以下の2つが使えます。
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
+ ignoredActions: [
+ 'issue/fetchIssueList/rejected', // この action に対しては serializableCheck しない
+ ]
},
}),
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
+ ignoredActionPaths: ['payload'] // action.payload に対しては serializableCheck しない
]
},
}),
これを設定しておくことで解決できます。
ですが、この方法は採用されませんでした。理由は次で話します。
3-2. Error を dispatch しない
そもそもの話ですが、Error 本体を dispatch しなければいいということに(今更)気づきました。
createAsyncThunk
のエラー処理で使われてるrejectWithValue
の型を確認してみましょう。
// rejectValue を unknown に定義してたので value が unknown になってる
rejectWithValue: (value: unknown) => RejectWithValue<unknown, unknown>
これだけだと情報が足りないのでnode_modules/@reduxjs/toolkit
にジャンプしてRejectWithValue
も確認します。
class RejectWithValue<Payload, RejectedMeta> {
/*
type-only property to distinguish between RejectWithValue and FulfillWithMeta
does not exist at runtime
*/
private readonly _type!: 'RejectWithValue'
constructor(
public readonly payload: Payload, // Payload は rejectWithValue の第一引数の型
public readonly meta: RejectedMeta
) {}
}
色々ありますが、要はRejectWithValue
に第一引数として渡したやつが rejected action のpayload
に入るということでしょう。
そして私は(何も考えずに)rejectWithValue(error)
と書いてたので、しっかり action に non-serializable value を突っ込んでたということです! そんなもん入れてねぇぞと思ってたのが恥ずかしくなった
ここまでわかったら解決は簡単です。
まず payload にエラーメッセージ(string
)だけ入れたいのでrejectValue
の型を修正します。
export type ThunkAPI = {
dispatch: AppDispatch;
- rejectValue: unknown;
+ rejectValue: string;
state: RootState;
};
+) ThunkAPI の型が何なのか分からない方はこの記事をご参考ください。
次はエラーをカスタマイズします。catch 文のエラーの型はunknown
になってるので、そこからstring
を取り出して返す関数を作成します。
export const customizeError = (error: unknown): string => {
if (error instanceof Error) return error.message;
return '未知のエラーが発生しました';
};
そしてrejectWithValue
に入れれば完成!
export const fetchIssueList = createAsyncThunk<Issue[], InputValues, ThunkAPI>(
'issue/fetchIssueList',
async (getIssueListParam, { rejectWithValue }) => {
try {
const { data } = await getIssueList(getIssueListParam);
return formatIssueList(data);
} catch (error) {
return rejectWithValue(customizeError(error)); // エラーメッセージ(string)を payload に入れる
}
});
これでstore.ts
の設定触らずにエラー解決できました。何も考えずにコード書くのやめろっていう話ですね^_^...
4.終わり
このエラーをググってみたら action に timestamp を入れた場合がよく出てきて、解決案で3-1. serializableCheckを外す
で紹介した方法が挙げられてました。
「Do Not Put Non-Serializable Values in State or Actions」にも action が reducer に至る前に thunk みたいなミドルウェアに渡されるときは例外だと載ってますので、そういうとき使えるんじゃないかと思います。
Exception: you may put non-serializable values in actions if the action will be intercepted and stopped by a middleware before it reaches the reducers. Middleware such as redux-thunk and redux-promise are examples of this.
でも JSON.stringify()、JSON.parse() で値を整形したら解決できそうな気がしたので、今度そういう場面に出くわしたら試してみます。
+)余談
2021年頑張って書いた記事、思ったよりたくさんの方々が読んでくださってビックリしました!!! 1年間なんとサポートバッジ3! いいね1,318! ページビュー81,957!!!
本当にありがとうございます(´⌣`ʃƪ) 今年も頑張りますー!!!
Discussion