Redux切片间共享Action引发循环依赖问题求助
我在Redux中碰到了循环依赖问题,根源是切片(slice)之间共享Action。
我在authActions.js里写了authFetch函数,它会在请求时检查认证信息,必要时自动刷新。这个函数需要用到auth reducer,而且要在和authSlice无关的asyncThunkAction里调用。
问题出在:authFetch里必须用store.dispatch(),但在authActions.js里导入store会造成循环依赖——因为store本身也导入了各个slice文件。
相关代码示例
authActions.js
import { store } from "@/redux/store"; // 导入Redux store import { createAsyncThunk } from "@reduxjs/toolkit"; import Cookies from "js-cookie"; import AppError from "@/utils/AppError"; // async Thunks export const logIn = createAsyncThunk( "login", async (form, { rejectWithValue }) => { try { const response = await fetch( `${process.env.NEXT_PUBLIC_API_URL}/users/login/`, { credentials: "include", method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(form), } ); console.log(response); const data = await response.json(); if (!response.ok) { return rejectWithValue(data); } else { return data; } } catch (err) { console.log(err); return rejectWithValue(err); } } ); export const logOut = createAsyncThunk( "logOut", async (_, { rejectWithValue }) => { try { const response = await fetch( `${process.env.NEXT_PUBLIC_API_URL}/users/logOut`, { method: "DELETE", credentials: "include", } ); const data = await response.json(); Cookies.remove("token"); if (!response.ok) { return rejectWithValue(data); } return data; } catch (err) { return rejectWithValue(err); } } ); export const getAuth = createAsyncThunk( "getAuth", async (_, { rejectWithValue }) => { try { const response = await fetch( `${process.env.NEXT_PUBLIC_API_URL}/users/refreshToken/`, { credentials: "include", method: "GET", } ); const data = await response.json(); if (!response.ok) { return rejectWithValue(data); } return data; } catch (err) { console.log(err); return rejectWithValue(err); } } ); function authToken() { return store.getState().auth.data.token; } function addAuthTokenToFetchOptions(fetchOptions) { const fetchOptionsWithAuth = { ...fetchOptions, headers: { ...(fetchOptions.headers || {}), Authorization: `Bearer ${authToken()}`, }, }; return fetchOptionsWithAuth; } export const authFetch = async (url, fetchOptions) => { fetchOptions = addAuthTokenToFetchOptions(fetchOptions); const response = await fetch(url, fetchOptions); if (response.ok) { const data = await response.json(); return data; } else { // 如果响应状态是401/403,说明access token无效,尝试用refresh token刷新 if ([401, 403].includes(response.status)) { const refreshTokenResponse = await fetch( `${process.env.NEXT_PUBLIC_API_URL}/users/refreshToken`, { method: "GET", credentials: "include", } ); // 如果成功获取新的access token,更新auth reducer并重试原请求 if (refreshTokenResponse.ok) { const credentialsData = await refreshTokenResponse.json(); store.dispatch(setCredentials(credentialsData)); fetchOptions = addAuthTokenToFetchOptions(fetchOptions); const retryResponse = await fetch(url, fetchOptions); if (retryResponse.ok) { const data = await retryResponse.json(); return data; } else { throw new AppError(data); } } else { // 如果refresh token也无效,执行登出 if ([401, 403].includes(refreshTokenResponse.status)) { store.dispatch(logOut()); } throw new AppError(data); } } else { throw new AppError(data); } } };
authFetch在restaurantActions.js中的使用示例
import { createAsyncThunk } from "@reduxjs/toolkit"; import { authFetch } from "../auth/authActions"; export const postRestaurantSettings = createAsyncThunk( "postRestaurantSettings", async (payload) => { try { const data = await authFetch( `${process.env.NEXT_PUBLIC_API_URL}/restaurants/updateRestaurantSettings`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), } ); return data; } catch (error) { console.error(error); } } );
store.js
import { persistStore, persistReducer } from "redux-persist"; import { combineReducers, configureStore } from "@reduxjs/toolkit"; import createWebStorage from "redux-persist/lib/storage/createWebStorage"; import restaurant from "@/redux/restaurant/restaurantSlice"; import cart from "@/redux/cart/cartSlice"; import auth from "@/redux/auth/authSlice"; // 解决服务端渲染时redux-persist无法创建localStorage的问题 const createNoopStorage = () => { return { getItem(_key) { return Promise.resolve(null); }, setItem(_key, value) { return Promise.resolve(value); }, removeItem(_key) { return Promise.resolve(); }, }; }; const storage = typeof window !== "undefined" ? createWebStorage("local") : createNoopStorage(); const reducers = combineReducers({ restaurant, cart, auth }); const persistConfig = { key: "root", storage, blacklist: ["auth"] }; export const store = configureStore({ reducer: persistReducer(persistConfig, reducers), middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: false }), }); export const persistor = persistStore(store);
循环依赖错误信息
ReferenceError: Cannot access '__WEBPACK_DEFAULT_EXPORT__' before initialization
我知道Redux Thunk API可以解决这个问题,但不想增加项目配置复杂度,请问还有其他可行的解决方案?
方案1:延迟导入store(动态导入)
把authActions.js里的store导入改成动态导入,避免初始化时的循环引用。修改authToken和authFetch里的store使用逻辑:
// 移除顶部的store导入 // import { store } from "@/redux/store"; async function authToken() { // 动态导入store const { store } = await import("@/redux/store"); return store.getState().auth.data.token; } async function addAuthTokenToFetchOptions(fetchOptions) { const token = await authToken(); const fetchOptionsWithAuth = { ...fetchOptions, headers: { ...(fetchOptions.headers || {}), Authorization: `Bearer ${token}`, }, }; return fetchOptionsWithAuth; } export const authFetch = async (url, fetchOptions) => { // 这里要await获取带token的配置 fetchOptions = await addAuthTokenToFetchOptions(fetchOptions); const response = await fetch(url, fetchOptions); if (response.ok) { const data = await response.json(); return data; } else { if ([401, 403].includes(response.status)) { const refreshTokenResponse = await fetch( `${process.env.NEXT_PUBLIC_API_URL}/users/refreshToken`, { method: "GET", credentials: "include", } ); if (refreshTokenResponse.ok) { const credentialsData = await refreshTokenResponse.json(); // 动态导入store并dispatch const { store } = await import("@/redux/store"); store.dispatch(setCredentials(credentialsData)); // 重新获取带新token的配置 fetchOptions = await addAuthTokenToFetchOptions(fetchOptions); const retryResponse = await fetch(url, fetchOptions); if (retryResponse.ok) { const data = await retryResponse.json(); return data; } else { throw new AppError(data); } } else { if ([401, 403].includes(refreshTokenResponse.status)) { const { store } = await import("@/redux/store"); store.dispatch(logOut()); } throw new AppError(data); } } else { throw new AppError(data); } } };
原理:动态导入会在函数执行时才加载store模块,此时store已经完成初始化,避免了初始化阶段的循环引用。
方案2:将authFetch封装成自定义Hook(仅React环境)
如果你的项目是React项目,可以把authFetch改成自定义Hook,直接通过useDispatch和useSelector获取Redux的dispatch和状态,完全避免导入store:
创建useAuthFetch.js:
import { useDispatch, useSelector } from "react-redux"; import { setCredentials, logOut } from "@/redux/auth/authActions"; import AppError from "@/utils/AppError"; export const useAuthFetch = () => { const dispatch = useDispatch(); const token = useSelector(state => state.auth.data.token); const addAuthTokenToFetchOptions = (fetchOptions) => { return { ...fetchOptions, headers: { ...(fetchOptions.headers || {}), Authorization: `Bearer ${token}`, }, }; }; const authFetch = async (url, fetchOptions) => { let options = addAuthTokenToFetchOptions(fetchOptions); const response = await fetch(url, options); if (response.ok) { return await response.json(); } else { if ([401, 403].includes(response.status)) { const refreshTokenResponse = await fetch( `${process.env.NEXT_PUBLIC_API_URL}/users/refreshToken`, { method: "GET", credentials: "include", } ); if (refreshTokenResponse.ok) { const credentialsData = await refreshTokenResponse.json(); dispatch(setCredentials(credentialsData)); // 重新获取新token后的配置 options = addAuthTokenToFetchOptions(fetchOptions); const retryResponse = await fetch(url, options); if (retryResponse.ok) { return await retryResponse.json(); } else { throw new AppError(await retryResponse.json()); } } else { if ([401, 403].includes(refreshTokenResponse.status)) { dispatch(logOut()); } throw new AppError(await refreshTokenResponse.json()); } } else { throw new AppError(await response.json()); } } }; return authFetch; };
在组件中使用示例:
import { useAuthFetch } from "./useAuthFetch"; import { useDispatch } from "react-redux"; const SettingsComponent = () => { const authFetch = useAuthFetch(); const dispatch = useDispatch(); const handleSaveSettings = async (payload) => { try { const data = await authFetch( `${process.env.NEXT_PUBLIC_API_URL}/restaurants/updateRestaurantSettings`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), } ); dispatch({ type: "postRestaurantSettings/fulfilled", payload: data }); } catch (error) { console.error(error); } }; return <button onClick={() => handleSaveSettings({/* payload */})}>保存设置</button>; };
原理:利用React Redux的Hook直接获取dispatch和状态,不需要导入store,从根源上消除循环依赖。
方案3:将store实例注入到authFetch中
修改authFetch,让它接收dispatch和getState作为参数,或者在应用初始化时注入store实例:
- 修改
authActions.js,移除store导入,把authFetch改成工厂函数:
export const createAuthFetch = (dispatch, getState) => { function authToken() { return getState().auth.data.token; } function addAuthTokenToFetchOptions(fetchOptions) { const fetchOptionsWithAuth = { ...fetchOptions, headers: { ...(fetchOptions.headers || {}), Authorization: `Bearer ${authToken()}`, }, }; return fetchOptionsWithAuth; } return async (url, fetchOptions) => { fetchOptions = addAuthTokenToFetchOptions(fetchOptions); const response = await fetch(url, fetchOptions); if (response.ok) { const data = await response.json(); return data; } else { if ([401, 403].includes(response.status)) { const refreshTokenResponse = await fetch( `${process.env.NEXT_PUBLIC_API_URL}/users/refreshToken`, { method: "GET", credentials: "include", } ); if (refreshTokenResponse.ok) { const credentialsData = await refreshTokenResponse.json(); dispatch(setCredentials(credentialsData)); fetchOptions = addAuthTokenToFetchOptions(fetchOptions); const retryResponse = await fetch(url, fetchOptions); if (retryResponse.ok) { const data = await retryResponse.json(); return data; } else { throw new AppError(data); } } else { if ([401, 403].includes(refreshTokenResponse.status)) { dispatch(logOut()); } throw new AppError(data); } } else { throw new AppError(data); } } }; };
- 在store初始化后,创建authFetch实例并导出:
修改store.js:
// 导入createAuthFetch和所需action import { createAuthFetch, setCredentials, logOut } from "@/redux/auth/authActions"; // ... 原store代码 ... // 创建authFetch实例 export const authFetch = createAuthFetch(store.dispatch, store.getState);
- 在其他地方使用时,直接导入store.js中的authFetch:
import { authFetch } from "@/redux/store"; export const postRestaurantSettings = createAsyncThunk( "postRestaurantSettings", async (payload) => { try { const data = await authFetch( `${process.env.NEXT_PUBLIC_API_URL}/restaurants/updateRestaurantSettings`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), } ); return data; } catch (error) { console.error(error); } } );
原理:把store的dispatch和getState作为参数传入authFetch,避免在authActions中直接导入store,打破循环依赖链。
内容的提问来源于stack exchange,提问作者Poumon

