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 应该返回一个带有 getItemsetItemremoveItem 的 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 调用的 getItemsetItemremoveItem 一定存在,并且返回 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

浏览器访问的还是旧端口或旧缓存,所以即使代码已经改了,控制台仍然继续报旧错误。

处理方式:

  1. 停掉旧的 pnpm dev 进程。
  2. 重新启动一个干净的开发服务。
  3. 浏览器强制刷新:Cmd + Shift + R
  4. 如果还不行,打开 DevTools 后右键刷新按钮,选择 Empty Cache and Hard Reload

排查 Checklist

遇到类似问题时,可以按这个顺序检查:

  1. 确认 persistConfig.storage 是否真的有 getItemsetItemremoveItem
  2. 不确定默认导入是否正常时,直接手写 storage adapter。
  3. configureStore 中忽略 redux-persist 的内部 action。
  4. main.tsx 中确认已经使用 PersistGate
  5. 确认浏览器访问的是当前正在运行的 Vite 端口。
  6. 停掉旧服务,清理浏览器缓存后重新访问。

总结

这类错误表面上看是 redux-persist 报错,本质通常是 storage adapter 没有正确传入。

在 Vite + ESM 环境下,如果 redux-persist/lib/storage 的默认导出出现兼容问题,直接手写一个符合 WebStorage 接口的 adapter 是最稳定的解决方案。

同时,Redux Toolkit 默认的序列化检查需要对 redux-persist 的内部 action 做白名单处理,否则会出现非序列化警告。

(以上内容为AI生成,仅供参考)

posted on 2026-06-01 22:21  朝朝暮Mu  阅读(34)  评论(0)    收藏  举报