首页 > 解决方案 > React loader 在“挂起”的 axios 请求期间不会保持可见

问题描述

我正在使用反应上下文和 axios 拦截器来隐藏/显示加载微调器。虽然在触发请求和响应时它会正确隐藏和显示,但我的加载微调器仅在网络请求开始时显示。请求触发,然后进入“待处理”状态。在“待定”状态期间,响应拦截器被触发并隐藏加载微调器。

如何确保加载微调器在待处理请求期间保持可见?

我尝试添加一些控制台日志以在返回请求、响应和错误(包括计数)时触发,并且它显示(对于两个请求)成功地从 0 - 1 - 2 - 1 - 0 开始,但在 chrome devtools即使所有请求都已返回,网络选项卡仍显示为待处理。

编辑:以为我在重构后让它工作了,但这是不行的。添加了更新的代码

import React, { useReducer, useRef, useEffect, useCallback } from "react";
import { api } from "api/api";
import LoadingReducer from "reducer/LoadingReducer";

const LoadingContext = React.createContext();

export const LoadingProvider = ({ children }) => {
  const [loader, dispatch] = useReducer(LoadingReducer, {
    loading: false,
    count: 0,
  });

  const loaderKeepAlive = useRef(null),
    showLoader = useRef(null);

  const showLoading = useCallback(() => {
    dispatch({
      type: "SHOW_LOADING",
    });
  }, [dispatch]);

  const hideLoading = useCallback(() => {
    loaderKeepAlive.current = setTimeout(() => {
      dispatch({
        type: "HIDE_LOADING",
      });
    }, 3000);
    return clearTimeout(loaderKeepAlive.current);
  }, [dispatch]);

  const requestHandler = useCallback(
    (request) => {
      dispatch({ type: "SET_COUNT", count: 1 });
      return Promise.resolve({ ...request });
    },
    [dispatch]
  );

  const errorHandler = useCallback(
    (error) => {
      dispatch({ type: "SET_COUNT", count: -1 });
      return Promise.reject({ ...error });
    },
    [dispatch]
  );

  const successHandler = useCallback(
    (response) => {
      dispatch({ type: "SET_COUNT", count: -1 });
      return Promise.resolve({ ...response });
    },
    [dispatch]
  );

  useEffect(() => {
    if (loader.count === 0) {
      hideLoading();
      clearTimeout(showLoader.current);
    } else {
      showLoader.current = setTimeout(() => {
        showLoading();
      }, 1000);
    }
  }, [showLoader, showLoading, hideLoading, loader.count]);

  useEffect(() => {
    if (!api.interceptors.request.handlers[0]) {
      api.interceptors.request.use(
        (request) => requestHandler(request),
        (error) => errorHandler(error)
      );
    }
    if (!api.interceptors.response.handlers[0]) {
      api.interceptors.response.use(
        (response) => successHandler(response),
        (error) => errorHandler(error)
      );
    }
    return () => {
      clearTimeout(showLoader.current);
    };
  }, [errorHandler, requestHandler, successHandler, showLoader]);

  return (
    <LoadingContext.Provider
      value={{
        loader,
      }}
    >
      {children}
    </LoadingContext.Provider>
  );
};

export default LoadingContext;

标签: javascriptreactjsaxios

解决方案


我认为更标准的方法是仅利用loading状态来有条件地渲染 Spinner 以及将其从 DOM 中删除的承诺的结果(参见下面的演示)。通常,拦截器用于从 API 响应返回错误,因为 axios 默认为状态错误(例如,404 - not found)。

例如,自定义 axios 拦截器来显示 API 错误:

import get from "lodash.get";
import axios from "axios";

const { baseURL } = process.env;

export const app = axios.create({
  baseURL
});

app.interceptors.response.use(
  response => response,
  error => {
    const err = get(error, ["response", "data", "err"]);

    return Promise.reject(err || error.message);
  }
);

export default app;

演示

编辑条件渲染

代码

应用程序.js

import React, { useEffect, useCallback, useState } from "react";
import fakeApi from "./api";
import { useAppContext } from "./AppContext";
import Spinner from "./Spinner";

const App = () => {
  const { isLoading, error, dispatch } = useAppContext();
  const [data, setData] = useState({});

  const fetchData = useCallback(async () => {
    try {
      // this example uses a fake api
      // if you want to trigger an error, then pass a status code other than 200
      const res = await fakeApi.get(200);
      setData(res.data);
      dispatch({ type: "loaded" });
    } catch (error) {
      dispatch({ type: "error", payload: error.toString() });
    }
  }, [dispatch]);

  const reloadData = useCallback(() => {
    dispatch({ type: "reset" });
    fetchData();
  }, [dispatch, fetchData]);

  useEffect(() => {
    fetchData();

    // optionally reset context state on unmount
    return () => {
      dispatch({ type: "reset" });
    };
  }, [dispatch, fetchData]);

  if (isLoading) return <Spinner />;
  if (error) return <p style={{ color: "red" }}>{error}</p>;

  return (
    <div style={{ textAlign: "center" }}>
      <pre
        style={{
          background: "#ebebeb",
          margin: "0 auto 20px",
          textAlign: "left",
          width: 600
        }}
      >
        <code>{JSON.stringify(data, null, 4)}</code>
      </pre>
      <button type="button" onClick={reloadData}>
        Reload
      </button>
    </div>
  );
};

export default App;

AppContext.js

import React, { createContext, useContext, useReducer } from "react";

const AppContext = createContext();

const initialReducerState = {
  isLoading: true,
  error: ""
};

const handleLoading = (state, { type, payload }) => {
  switch (type) {
    case "loaded":
      return { isLoading: false, error: "" };
    case "error":
      return { isLoading: false, error: payload };
    case "reset":
      return initialReducerState;
    default:
      return state;
  }
};

export const AppContextProvider = ({ children }) => {
  const [state, dispatch] = useReducer(handleLoading, initialReducerState);

  return (
    <AppContext.Provider
      value={{
        ...state,
        dispatch
      }}
    >
      {children}
    </AppContext.Provider>
  );
};

export const useAppContext = () => useContext(AppContext);

export default AppContextProvider;

Spinner.js

import React from "react";

const Spinner = () => <div className="loader">Loading...</div>;

export default Spinner;

fakeApi.js

const data = [{ id: "1", name: "Bob" }];

export const fakeApi = {
  get: (status) =>
    new Promise((resolve, reject) => {
      setTimeout(() => {
        status === 200
          ? resolve({ data })
          : reject(new Error("Unable to locate data."));
      }, 2000);
    })
};

export default fakeApi;

index.js

import React from "react";
import ReactDOM from "react-dom";
import AppContextProvider from "./AppContext";
import App from "./App";
import "./styles.css";

const rootElement = document.getElementById("root");
ReactDOM.render(
  <React.StrictMode>
    <AppContextProvider>
      <App />
    </AppContextProvider>
  </React.StrictMode>,
  rootElement
);

推荐阅读