You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Redux切片间共享Action引发循环依赖问题求助

问题: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实例:

  1. 修改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);
      }
    }
  };
};
  1. 在store初始化后,创建authFetch实例并导出:
    修改store.js:
// 导入createAuthFetch和所需action
import { createAuthFetch, setCredentials, logOut } from "@/redux/auth/authActions";
// ... 原store代码 ...

// 创建authFetch实例
export const authFetch = createAuthFetch(store.dispatch, store.getState);
  1. 在其他地方使用时,直接导入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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 04:32:35