Vite + React + Redux Persist 启动报错排查笔记
背景
项目是一个 Vite + React + Redux Toolkit 应用。在接入 redux-persist 持久化登录状态时,页面启动失败,并且浏览器控制台出现了下面几个错误:
Uncaught TypeError: storage.getItem is not a function
Uncaught TypeError: storage.setItem is not a function
同时还出现了 Redux Toolkit 的非序列化警告:
A non-serializable value was detected in an action, in the path: `err`.
Value: Error: redux-persist: persist timed out for persist key "root"
问题一:storage.getItem is not a function
最开始的写法类似这样:
import localStorage from "redux-persist/lib/storage";
const persistConfig = {
key: "root",
storage: localStorage,
whitelist: ["user"],
};
理论上 redux-persist/lib/storage 应该返回一个带有 getItem、setItem、removeItem 的 storage adapter。
但在当前 Vite 环境里,运行时拿到的对象并不是预期的 storage adapter,导致 redux-persist 内部调用:
storage.getItem(...)
时报错:
storage.getItem is not a function
后续写入持久化数据时,也会继续报:
storage.setItem is not a function
修复方式:手写 WebStorage Adapter
最稳的处理方式是不再依赖 redux-persist/lib/storage 的默认导出,而是直接提供一个符合 redux-persist 要求的 storage 对象。
import type {WebStorage} from "redux-persist/es/types";
const storage: WebStorage = {
getItem: (key: string) => Promise.resolve(window.localStorage.getItem(key)),
setItem: (key: string, value: string) => Promise.resolve(window.localStorage.setItem(key, value)),
removeItem: (key: string) => Promise.resolve(window.localStorage.removeItem(key)),
};
然后在 persistConfig 中使用这个 storage:
const persistConfig: PersistConfig<RootState> = {
key: "root",
storage,
whitelist: ["user"],
};
这样可以保证 redux-persist 调用的 getItem、setItem、removeItem 一定存在,并且返回 Promise,符合 redux-persist 的预期。
问题二:非序列化 action 警告
接入 redux-persist 后,Redux Toolkit 可能会出现下面的警告:
A non-serializable value was detected in an action, in the path: `err`.
这是因为 redux-persist 会派发一些内部 action,例如:
persist/PERSIST
persist/REHYDRATE
persist/FLUSH
persist/PAUSE
persist/PURGE
persist/REGISTER
其中某些 action 里可能携带函数或 Error 对象,而 Redux Toolkit 默认开启了 serializableCheck,所以会报警。
修复方式:忽略 redux-persist 内部 action
在 configureStore 里加入 serializableCheck.ignoredActions:
import {
FLUSH,
PAUSE,
PERSIST,
PURGE,
REGISTER,
REHYDRATE,
persistReducer,
persistStore,
} from "redux-persist";
const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) => getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}),
devTools: true,
});
这不是关闭整个序列化检查,而是只忽略 redux-persist 自己的内部 action,其他业务 action 仍然会被正常检查。
完整 store.ts 示例
import {combineReducers, configureStore, type Reducer} from "@reduxjs/toolkit";
import {
FLUSH,
PAUSE,
PERSIST,
PURGE,
REGISTER,
REHYDRATE,
persistReducer,
persistStore,
} from "redux-persist";
import type {PersistConfig} from "redux-persist";
import type {WebStorage} from "redux-persist/es/types";
import {languageReducer} from "./language/reducers";
import {reducer as productsReducer} from "./products/reducers";
import detailSlice from "./detail/slice";
import {productSearchSlice} from "./search/slice";
import {signInSlice} from "./signin/slice";
const storage: WebStorage = {
getItem: (key: string) => Promise.resolve(window.localStorage.getItem(key)),
setItem: (key: string, value: string) => Promise.resolve(window.localStorage.setItem(key, value)),
removeItem: (key: string) => Promise.resolve(window.localStorage.removeItem(key)),
};
const rootReducer = combineReducers({
language: languageReducer,
products: productsReducer,
detail: detailSlice.reducer,
search: productSearchSlice.reducer,
user: signInSlice.reducer,
});
export type RootState = ReturnType<typeof rootReducer>;
const persistConfig: PersistConfig<RootState> = {
key: "root",
storage,
whitelist: ["user"],
};
const persistedReducer = persistReducer<RootState>(
persistConfig,
rootReducer as unknown as Reducer<RootState>,
);
const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) => getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}),
devTools: true,
});
export type AppDispatch = typeof store.dispatch;
export const persistor = persistStore(store as any);
export default store;
main.tsx 中接入 PersistGate
store.ts 配好以后,还需要在 React 入口文件中接入 PersistGate:
import {StrictMode} from "react";
import {createRoot} from "react-dom/client";
import {Provider} from "react-redux";
import {PersistGate} from "redux-persist/integration/react";
import App from "./App";
import store, {persistor} from "./redux/store";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App/>
</PersistGate>
</Provider>
</StrictMode>,
);
另一个容易踩的坑:旧 Vite 服务和浏览器缓存
这次排查过程中还有一个干扰因素:本地同时启动了多个 Vite 服务,比如:
localhost:5173
localhost:5174
localhost:5175
localhost:5176
浏览器访问的还是旧端口或旧缓存,所以即使代码已经改了,控制台仍然继续报旧错误。
处理方式:
- 停掉旧的
pnpm dev进程。 - 重新启动一个干净的开发服务。
- 浏览器强制刷新:
Cmd + Shift + R。 - 如果还不行,打开 DevTools 后右键刷新按钮,选择
Empty Cache and Hard Reload。
排查 Checklist
遇到类似问题时,可以按这个顺序检查:
- 确认
persistConfig.storage是否真的有getItem、setItem、removeItem。 - 不确定默认导入是否正常时,直接手写 storage adapter。
configureStore中忽略redux-persist的内部 action。main.tsx中确认已经使用PersistGate。- 确认浏览器访问的是当前正在运行的 Vite 端口。
- 停掉旧服务,清理浏览器缓存后重新访问。
总结
这类错误表面上看是 redux-persist 报错,本质通常是 storage adapter 没有正确传入。
在 Vite + ESM 环境下,如果 redux-persist/lib/storage 的默认导出出现兼容问题,直接手写一个符合 WebStorage 接口的 adapter 是最稳定的解决方案。
同时,Redux Toolkit 默认的序列化检查需要对 redux-persist 的内部 action 做白名单处理,否则会出现非序列化警告。
(以上内容为AI生成,仅供参考)
浙公网安备 33010602011771号