The site

preface

Axios is currently the most popular front-end Ajax processing library. It is easy to use, extensible and full of functions.

I did not find a good way to deal with the encapsulation of AXIos in React, because AXIos is very independent and provides various apis that are one-time configurations, such as axios.create, axios.defaults, This makes it difficult for React to encapsulate Axios. It’s not that easy to use React in Axios and vice versa.

While there are various excellent React based Ajax wrappers, such as useRequest in SWR and Ahooks, they are more like handling async_function or promises than actual HTTP requests. Simply put, there is no native support for these two libraries until a request header is configured, because they both use FETCH by default and do not encapsulate Ajax further.

Axios is defined in React

For React, Axios is a third-party tool, or service. We can’t use jQuery or Vue2 thinking to understand all the ways axios is used, such as the interceptor for Axios.

Whether in frameless pages or Vue, I prefer a one-time configuration of Axios, such as:

export const ajax = axios.create({
  // ...
});

// Interceptor - error request prompt
ajax.interceptors.request.use(
  (config) = > config,
  (error) = > {
    alert("Request error!");
    return Promise.reject(error); });Copy the code

I would even create a separate file for the Axios interceptor, then make a series of interceptors for login, request state loading, logging, etc., and then import directly in bulk using syntax like the following:

export const ajax = axios.create({
  // ...
});

interceptors.forEach(({ request, response, fail }) = > {
  ajax.interceptors.request.use(request, fail);
  ajax.interceptors.response.use(response, fail);
});
Copy the code

But this situation makes you GG in React. Because this encapsulates AXIos, you don’t get to enjoy all the features of React, such as Context, Ref, or third-party routing, etc. (Even if you can squeeze in these, the architecture is very coupled.)

Going back to the section title, this is because AXIos itself exists as a tool, and I’m used to it. But in React, Axios is not entirely a third-party tool, and its interceptors should be defined as services, the side effect code in React.

Make Axios service-based

Think of Axios as a service, and the way it is encapsulated in React will solve the problem. My general package is as follows:

import axios from "axios";
import { Fragment } from "react";

const ajax = axios.create({
  /* ... config */
});

export default ajax;

// Service encapsulation
function useAjaxEffect1() {}
function useAjaxEffect2() {}

// Service hook collection
export function useAjaxEffect() {
  useAjaxEffect1();
  useAjaxEffect2();
}

// Service fragment
export function AjaxEffectFragment() {
  useAjaxEffect();
  return <Fragment />;
}
Copy the code

In the code above, useAjaxEffect and AjaxEffectFragment depend on your usage scenario:

  • If your AXIos wrapper is global, just mount useAjaxEffect on index.jsx or app.jsx.

  • If your AXIos wrapper is based on a state library, or a third-party component, then you should use AjaxEffectFragment to populate the service fragment inside the dependent component. This is recommended.

How to use

Here are two classic examples:

  • Consume the context in the AXIos interceptor, using useContext

  • Use the React Router as a third-party route in AXIos

Consumption context

Consuming context in Axios has always been tricky in React, but with the encapsulation described above, the code is much simpler. In this example, we simulate request log listening and write the listening request to the Context and display it in the application.

First we need to write a logging context as follows:

// src/lib/log.tsx
import { createContext, useContext, useEffect, useRef, useState } from "react";

// Log template
const logTemplate = { log: [].update: (_log: string[]) = >{}};// Log context
export const LogContext = createContext(logTemplate);

// Log provisioning
export default function LogProvider({ children }) {
  const [log, setLog] = useState([]);

  return <LogContext.Provider value={{ log.update: setLog}} >{children}</LogContext.Provider>;
}

// Log service
export function useLog() {
  const { log, update } = useContext(LogContext);
  // Write log reference, write operation may be asynchronous, use ref can write the latest log status
  const writeRef = useRef<(newLog: string) = > void> (null);

  useEffect(() = > {
    writeRef.current = (newLog: string) = > void update([...log, newLog]);
  }, [log, update]);

  return { log, writeRef };
}
Copy the code

The log library contains the context provider LogProvider and the hook useLog that uses logs. It is very simple to use.

You may wonder why useRef is used to store the log-writing function. This is because writes can be asynchronous, especially in axios interceptors, which are bound to the context in which the request is executed. Asynchronous requests may write the log to the old state. I like to refer to this structure of binding real-time state as state tracking. See axios interceptor closure in the last section.

Of course, you don’t have to force useRef in useLog to export real-time update logs. You can just let the services that call the library do their own state tracking.

Next we go to app.tsx and write the following code:

import "./styles.css";
import ajax, { useAjaxEffect, AjaxEffectFragment } from "./lib/ajax";
import LogProvider, { useLog } from "./lib/log";

function Children1() {
  // You can use useAjaxEffect instead of 
      , but it is not recommended because updates to this component make useAjaxEffect redundant
  // useAjaxEffect();
  const { log } = useLog();

  async function handleFetch() {
    await ajax.get("https://raw.githubusercontent.com/facebook/react/main/README.md");
  }

  return (
    <div className="children">
      <h2>children 1</h2>
      <button onClick={handleFetch}>run axios</button>
      {log.map((v, i) => (
        <p key={i}>{v}</p>
      ))}
    </div>
  );
}

export default function App() {
  return (
    <LogProvider>
      <AjaxEffectFragment />
      <Children1 />
    </LogProvider>
  );
}
Copy the code

In app.tsx we applied itLogProvider, note that writing to the log in AXIos requires consuming the log library context, so axios side effect code must be placedLogProviderIn the.

Similarly, to call third-party libraries in AXIos, such as page routing, you need to put
in the router.

This sequence of steps has nothing to do with Axios, right? That’s what I like about React. It makes your code very decoupled.

Now we need to listen for the request in AXIos and write it to the log library. This is very simple. Let’s rewrite useAjaxEffect1 from the previous section as follows:

// Axios requests listening
function useAjaxEffect1() {
  const { writeRef } = useLog();

  useEffect(() = > {
    function request(config) {
      writeRef.current('New request:${config.url}`);
      return config;
    }

    function fail(error) {
      writeRef.current('Request failed:${error.message}`);
      return Promise.reject(error);
    }

    function success(response) {
      writeRef.current('Response successful:${response.config.url}`);
      return Promise.resolve(response);
    }

    const inter1 = ajax.interceptors.request.use(request, fail);
    const inter2 = ajax.interceptors.response.use(success, fail);

    return () = > {
      ajax.interceptors.request.eject(inter1);
      ajax.interceptors.response.eject(inter2);
    };
  }, [writeRef]);
}
Copy the code

At this point we let Axios come to life in React. The interceptor logs the request in the React context in real time, and we can call the logging context anywhere in React to view the request log.

You can check it out at Codesandbox.

Use routing in interceptors

Using routes in axios’s interceptor is also a hassle, and there are some “dirty” ways to handle routes. I used to do this, and I even used a rude one:

window.location.href = baseURL + "/404.html";
Copy the code

You may not get the latest route, or you may simply abandon the no-refresh route provided by the React-Router. All in all, MY use of routing in axios’s interceptor was never a disgrace.

But now we can do this by modifying the code in SRC/app.tsx as follows:

function DefaultPage() {
  async function handleFetch() {
    // This is an error URL, github will return 404 to us
    await ajax.get("https://reactjs.org/123/123");
  }
  return (
    <div>
      <h3>Default Page</h3>
      <button onClick={handleFetch}>fetch 404 data</button>
    </div>
  );
}

function Status404Page() {
  const history = useHistory();
  return (
    <div>
      <h2>404 Page</h2>
      <button onClick={()= > void history.goBack()}>back page</button>
    </div>
  );
}

function Children2() {
  return (
    <div className="children">
      <h2>children 2</h2>
      <Switch>
        <Route exact path="/">
          <DefaultPage />
        </Route>
        <Route path="/ 404">
          <Status404Page />
        </Route>
      </Switch>
    </div>
  );
}

export default function App() {
  return (
    <LogProvider>
      <BrowserRouter>
        <AjaxEffectFragment />
        <Children1 />
        <Children2 />
      </BrowserRouter>
    </LogProvider>
  );
}
Copy the code

In the code above, we added the react-Router and placed the Router
outside the
. You have to do that, otherwise you can’t use useHistory in Axios. React is a basic concept that we won’t discuss in detail.

We then route the page in the Children2 component, one/path, one /404 path.

In the DefaultPage component of the DefaultPage, we can make an error request and the request will return us a 404 status code. Now we need to intercept it in axios and redirect to /404 when a 404 occurs.

Again simple, let’s rewrite useAjaxEffect2 from the previous section as follows:

// 404 The request is redirected to page /404
function useAjaxEffect2() {
  const history = useHistory();
  const historyRef = useRef(history);

  See the last section [Axios interceptor closure]. I recommend doing this even though useHistory returns a reference value that does not change
  useEffect(() = > {
    historyRef.current = history;
  }, [history]);

  useEffect(() = > {
    function success(response) {
      return Promise.resolve(response);
    }

    function check404(error) {
      // A 404 request was detected to redirect the page
      if (error.response && error.response.status === 404) {
        historyRef.current.push("/ 404");
      }
      return Promise.reject(error);
    }

    const interId = ajax.interceptors.response.use(success, check404);

    return () = > void ajax.interceptors.response.eject(interId);
  }, [historyRef]);
}
Copy the code

All 404 requests now redirect to /404. In codesandBox, error requests display error messages on the page. You need to turn it off manually to see the final result.

You can now try clicking the button in the default page, which will make a 404 request and redirect the page to /404.

Axios interceptor closure

One very special aspect of the Axios interceptor is that an ongoing AXIos request cannot remove or add interceptors, which I call Axios interceptor closure.

State of loss

For example, in both of the above examples, I used Ref for reference calls to interceptor dependent functions. If I had used a non-reference function directly, such as the log update function in the logging example, or the history object in the route jump example, The interceptor will access the reference to the binding, and if the reference is updated during the request, the interceptor will not know.

What kind of problems does that create?

Assuming foo and bar are two requests, log is the log information, and the default is an empty array [], then we have the AXIos interceptor perform an update([…oldLog, newLog]) on the log array, starting the request with the request name, Request ends with “request name + end”, foo and bar are requested in the following order:

  1. Foo request start

  2. Bar request start

  3. Request foo End

  4. Bar request end

In this case, we would expect log = [foo, bar, fooEnd, barEnd], right, but this is not the case.

If Ref is not used for state tracking, the actual log written would be:

  1. Initial state: log = [] is denoted as state A;

  2. Log = […A, foo] = [foo];

  3. Log = […B, bar] = [foo, bar];

  4. Log = […A, fooEnd] = [fooEnd] state D; (Foo does not update state because the interceptor forms A closure for state A)

  5. Bar request ends and pushes from B: log = […B, barEnd] = [foo, barEnd].

Eventually [foo, barEnd] will be logged as a result, which is obviously not what we expect. So we need to use state tracking.

I wrote this bug on CodesandBox, thinking it was an environmental problem, but then I found out I was on the first floor, Axios was on the fifth floor, and codesandBox was in the clouds.

You can reproduce this bug by modifying the useLog service provided by the log library file:

// Log hooks
export function useLog() {
  const { log, update } = useContext(LogContext);
  // Write log reference, write operation may be asynchronous, use ref can write the latest log status
  const writeRef = useRef<(newLog: string) = > void> (null);

  useEffect(() = > {
    writeRef.current = (newLog: string) = > void update([...log, newLog]);
  }, [log, update]);

  const write = useCallback((newLog: string) = > void update([...log, newLog]), [log, update]);

  return { log, writeRef, write };
}
Copy the code

We add a write function that functions exactly as writeRef’s reference calculation logic. Use it, Daniel! You will feel the pain of bugs as much as I do.

Then use write to write the log, modify axios service useAjaxEffect1 part of the code as follows:

function useAjaxEffect1() {
  // Use write instead of writeRef altogether
  const { write } = useLog();

  useEffect(() = > {
    function request(config) {
      write('New request:${config.url}`);
      return config;
    }

    function fail(error) {
      write('Request failed:${error.message}`);
      return Promise.reject(error);
    }

    function success(response) {
      write('Response successful:${response.config.url}`);
      return Promise.resolve(response);
    }

    const inter1 = ajax.interceptors.request.use(request, fail);
    const inter2 = ajax.interceptors.response.use(success, fail);

    return () = > {
      ajax.interceptors.request.eject(inter1);
      ajax.interceptors.response.eject(inter2);
    };
  }, [write]); // Remember to change dependencies
}
Copy the code

At this point the bug reappears, and no matter how much you ask, the log writes always look weird. This is called state loss.

State tracking

I don’t know which team it is. They call each execution of React as an execution frame and each data used in the execution frame as frame data. I like the way it’s called.

React frame data always changes with the execution frame. The data in the last frame becomes the outdated frame in the next frame.

Axios interceptors are fixed at the start of a request and cannot be changed halfway through. These interceptors bind to the frame data of the execution frame at the start of the request, forming a closure. The interceptors are asynchronous, and it is not known how many frames will be executed in a request, resulting in state loss and the inability to update the frame data properly.

However, you can easily solve this problem by using state tracking, which is just a reference to frame data using useRef. The return value of useRef itself does not change, we can call it constant frame data, although the ref. Current is changed, but the ref reference itself does not change, so the reference does not change from the frame in which ref is declared.

For axios interceptor closures, we use useRef. By making ref a closure of a third-party API, React can control it precisely on every frame, changing the execution environment of third-party libraries.

The tail language

This is my new wrapper prototype for the Axios interceptor in React. If you have a better idea, please discuss it.