DEV Community

Scott Hu
Scott Hu

Posted on • Originally published at alova.js.org

You've kept that axios instance for years — do you really need to rewrite it?

You have an axios setup that works. Interceptors handle auth, a baseURL is set once, error handling lives in one place. Nobody wants to rewrite all of that to try a new library.

Good news: you don't have to. alova can drive your existing axios instance — interceptors, baseURL, timeout, the lot — and add pagination, caching, retries, and request sharing on top. axios still sends the request. alova decides how the request runs.

The setup you already have

Here's a typical list view in React with plain axios. It works, until you look at everything it doesn't do.

import axios from 'axios';
import { useState, useEffect } from 'react';

const api = axios.create({ baseURL: '/api' });

export function UserList() {
  const [page, setPage] = useState(1);
  const [data, setData] = useState([]);
  const [total, setTotal] = useState(0);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    api.get('/users', { params: { page, pageSize: 10 } })
      .then(res => {
        if (cancelled) return;
        setData(res.data.data);
        setTotal(res.data.total);
      })
      .catch(e => { if (!cancelled) setError(e); })
      .finally(() => { if (!cancelled) setLoading(false); });
    return () => { cancelled = true; };
  }, [page]);

  // Where's the caching? The dedupe of a re-clicked page?
  // A retry when the network blips? None of it exists yet.
}
Enter fullscreen mode Exit fullscreen mode

Three problems show up the moment this ships:

  1. Every mount refetches, even for data that hasn't changed.
  2. Click page 3, then quickly page 2 — the slower response can land last and show the wrong page.
  3. A transient 500 means the user gets a hard error instead of a second try.

You could patch each one by hand. People do. But the patches are the same in every project, and they're easy to get subtly wrong.

What you actually want

Strip it down and the wish list is small:

  • Keep the axios instance and its interceptors exactly as they are.
  • Get loading/error/data state without writing it per component.
  • Paginate without hand-rolling page state and race handling.
  • Cache GETs so re-mounts don't refetch.
  • Share one in-flight request across components that ask for the same thing.
  • Retry the few requests that really matter.

alova covers all six, and it uses axios to actually send the bytes. Here's the whole wiring change:

import axios from 'axios';
import { createAlova } from 'alova';
import { axiosRequestAdapter } from '@alova/adapter-axios';
import ReactHook from 'alova/react';
import { usePagination } from 'alova/client';

// untouched — your interceptors and baseURL stay
const api = axios.create({ baseURL: '/api' });

const alovaInst = createAlova({
  statesHook: ReactHook,
  requestAdapter: axiosRequestAdapter({ axios: api })
});

const getUsers = (page, pageSize) =>
  alovaInst.Get('/users', { params: { page, pageSize } });
Enter fullscreen mode Exit fullscreen mode

That's the migration. One instance, created once. Nothing about your axios config moves.

The same list, managed for you

export function UserList() {
  const {
    loading,
    error,
    data,
    page,
    pageSize,
    total,
    pageCount,
    isLastPage
  } = usePagination(getUsers, {
    initialPage: 1,
    initialPageSize: 10,
    initialData: { total: 0, data: [] }
  });

  if (loading) return <p>Loading…</p>;
  if (error) return <p>Could not load users.</p>;

  return (
    <div>
      {data.map(u => <Row key={u.id} user={u} />)}
      <button disabled={page <= 1} onClick={() => page.value--}>Prev</button>
      <button disabled={isLastPage} onClick={() => page.value++}>Next</button>
      <p>{page} / {pageCount} · {total} users</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

usePagination owns page state, drops stale responses when you switch pages fast, and knows when the list ends (isLastPage) so you stop requesting empty pages. Your axios interceptors still run on every one of these requests — alova's beforeRequest fires before axios's request interceptors, and responded fires after axios's response interceptors, so ordering is predictable.

For a single request, useRequest is the same idea with less surface:

const { loading, data, error } = useRequest(alovaInst.Get('/me'));
Enter fullscreen mode Exit fullscreen mode

The parts you didn't write

Four things are now handled that your hand-rolled version skipped:

Caching. GET responses are cached in memory for five minutes by default. Re-mount the component, hit the same URL — no second network call. Want a different window? cacheFor: 60 * 1000 on the method. Need it to survive a refresh? Switch the mode to restore and it lands in localStorage too, with tag-based invalidation when your data shape changes.

Request sharing. Two components asking for /me at the same time send one request, not two. It's on by default. (FormData bodies are the one exception — uploads are treated as intentional, so they never share.) Opt out per request with shareRequest: false.

Retries for the requests that matter. Most GETs shouldn't retry blindly, but a payment-status poll or a one-shot action can use one:

import { useRetriableRequest } from 'alova/client';

const { loading, data, error, send } = useRetriableRequest(
  alovaInst.Get('/order/status', { params: { id } }),
  { retry: 3, backoff: { delay: 2000, multiplier: 2 } }
);
Enter fullscreen mode Exit fullscreen mode

It defaults to three retries, one second apart, and you can make the delay grow exponentially or add jitter so a fleet of clients doesn't retry in lockstep.

What this costs you

Honesty over hype: this isn't free.

  • You add two packages: alova and @alova/adapter-axios.
  • Your team learns a Method abstraction (alovaInst.Get(...)) and the hook model. It's a small mental shift, but it's real.
  • You pick a statesHook per framework (React, Vue, Svelte). It's one line, but it's a line you didn't have before.
  • Interceptor order changes in a way that matters if your interceptors assume a specific sequence relative to alova's hooks. Read it once and you're fine, but don't assume it's identical to plain axios.

The upside is that migration is gradual. You wrap the instance, move one endpoint to alovaInst.Get, and leave the rest on api.get until you get to them. axios and alova run side by side during the switch.


Canonical source: the alova migration guide — https://alova.js.org/tutorial/project/migration/from-axios

Related reading

alova is on GitHub (alovajs/alova) and npm (alova). Full docs at https://alova.js.org.

Top comments (0)