DEV Community

Cover image for The evolution of fetch libraries: from Promise to strategies
Scott Hu
Scott Hu

Posted on

The evolution of fetch libraries: from Promise to strategies

Introduction

Frontend data fetching has gone through several paradigm shifts: from XMLHttpRequest callbacks, to Promise chains with fetch and axios, to declarative hooks with React Query and SWR. Each step traded more abstraction for less boilerplate.

A less-discussed but noteworthy direction is the move toward strategy-based request handling — where libraries don't just offer data-fetching hooks, but purpose-built hooks for specific business patterns: pagination, form submission, polling, file uploads, and more. Instead of telling the computer "how" to fetch data step by step, you declare "what" you need.

This article examines this evolution using alova as a reference implementation, analyzing when strategy-based approaches deliver value — and when simpler alternatives still make sense.

The Boilerplate Problem

Take paginated lists, one of the most common patterns in frontend development. Here's what a typical Promise-based implementation looks like with axios:

// Traditional Promise (axios) approach
import { useState, useEffect, useCallback } from 'react';
import axios from 'axios';

function TodoList() {
  const [list, setList] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [page, setPage] = useState(1);
  const pageSize = 10;
  const [total, setTotal] = useState(0);

  const fetchList = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const res = await axios.get('/api/todos', {
        params: { page, pageSize }
      });
      setList(res.data.list);
      setTotal(res.data.total);
    } catch (e) {
      setError(e.message);
    } finally {
      setLoading(false);
    }
  }, [page]);

  useEffect(() => { fetchList(); }, [fetchList]);

  return (
    <div>
      {loading && <Spinner />}
      {error && <ErrorMsg message={error} />}
      {list.map(item => <Item key={item.id} {...item} />)}
      <Pagination
        current={page}
        total={total}
        pageSize={pageSize}
        onChange={p => setPage(p)}
      />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This code works, but it has recurring problems:

  1. Boilerplate state management: loading, error, and data appear in nearly every data-fetching component
  2. Indirect dependency wiring: the useCallback dependency array + useEffect pattern adds cognitive overhead — the intent ("refetch when page changes") is buried under three layers of indirection
  3. Scattered refresh logic: triggering a refresh from outside the component (e.g., after creating a new item) requires lifting state or using refs
  4. Leaky pagination state: page, pageSize, and total are exposed and manually tracked

The Strategy-Based Approach

Promise-based code is imperative: you tell the computer how to do everything. Strategy-based code is declarative: you describe what you want, and the library handles execution.

alova implements this pattern with a set of scenario-specific hooks:

Strategy Hook Use Case
usePagination Paginated lists / infinite scroll
useForm Form submission (drafts, multi-step forms)
useAutoRequest Polling / focus refresh / reconnect refresh
useCaptcha Verification code + countdown
useUploader File uploads (progress, concurrency control)
useRetriableRequest Exponential backoff retry

Before / After: Three Common Scenarios

Scenario 1: Paginated Lists

// alova usePagination
import { usePagination } from 'alova/client';

function TodoList() {
  const {
    data,
    loading,
    error,
    page,
    pageSize,
    total,
    send
  } = usePagination(
    (page, pageSize) => alovaInstance.Get('/api/todos', {
      params: { page, pageSize }
    })
  );

  // All states managed by the hook
  return (
    <div>
      {loading && <Spinner />}
      {error && <ErrorMsg message={error.message} />}
      {data.map(item => <Item key={item.id} {...item} />)}
      <Pagination
        current={page}
        total={total}
        pageSize={pageSize}
        onChange={p => send(p)}
      />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Compared to the Promise version:

  • Five state variables collapsed into one hook call
  • No useCallback + useEffect dependency wiring
  • send(p) is semantically clearer than setPage(p)
  • Built-in page tracking, preloading, and cache invalidation

Scenario 2: Form Submission

// Traditional Promise approach
function LoginForm() {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError(null);
    try {
      await axios.post('/api/login', formData);
      router.push('/dashboard');
    } catch (e) {
      setError(e.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {error && <Alert type="error">{error}</Alert>}
      <button disabled={loading}>Login</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode
// alova useForm
function LoginForm() {
  const { loading, error, send } = useForm(
    (formData) => alovaInstance.Post('/api/login', formData),
    { resetAfterSubmitting: true }
  );

  return (
    <form onSubmit={e => send(e)}>
      {error && <Alert type="error">{error.message}</Alert>}
      <button disabled={loading}>Login</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

useForm encapsulates the loading/error management and try-catch-finally boilerplate. Additional capabilities like draft persistence and multi-step form state sharing are available through configuration — no extra code required.

Scenario 3: Polling

// Traditional: useEffect + setInterval
useEffect(() => {
  const fetchStatus = async () => {
    const { data } = await axios.get('/api/status');
    setStatus(data);
  };
  fetchStatus();
  const timer = setInterval(fetchStatus, 3000);
  return () => clearInterval(timer);
}, []);

// alova useAutoRequest
useAutoRequest(
  () => alovaInstance.Get('/api/status'),
  {
    pollingInterval: 3000,
    enablePolling: true
  }
);
Enter fullscreen mode Exit fullscreen mode

Beyond polling, useAutoRequest bakes in automatic refresh on tab focus and reconnection — features that would require separate visibilitychange and network event listeners with plain Promise code.

Three Technical Characteristics

1. Separation of Concerns

Strategy hooks decouple request execution logic from UI components. usePagination handles pagination state, data aggregation, and cache policies internally — the component only worries about rendering. This separation makes unit testing simpler: test component rendering and data logic independently.

2. Framework Agnostic

alova uses a StatesHook adapter layer to support React, Vue, Svelte, and other frameworks. The same usePagination / useForm API works consistently across frameworks, reducing cognitive overhead in multi-stack teams.

3. Composability

Strategies can be composed. For example, usePagination data can trigger refreshes from other components via actionDelegationMiddleware, and useForm can preload the next page's data with useFetcher after a successful submission.

Trade-Off Analysis

Every abstraction has a cost. Here's when strategy-based approaches help — and when they don't.

When It Works Well

  • Standard CRUD pages: list, form, and detail views with predictable patterns — strategy hooks cover most needs out of the box
  • Medium to large projects: many pages, complex request patterns, and frequent team collaboration — a unified strategy API promotes consistency and reduces review overhead
  • Multi-platform projects: applications targeting web, mini-programs, and mobile simultaneously — the framework-agnostic strategy layer reduces adaptation work
  • Projects needing caching: scenarios involving L1 (memory) + L2 (persistent) caching, auto-invalidation, and request deduplication — the built-in cache management reduces opportunities for manual errors

When to Think Twice

  • Trivially simple pages: a standalone page with a single GET request — the abstraction overhead exceeds the benefit; plain fetch or axios is lighter
  • Highly custom request flows: when your request pattern diverges significantly from built-in strategies, a custom implementation may be more straightforward (alova supports custom strategy hooks, but they require additional development)
  • Projects with established solutions: if your project already integrates deeply with React Query or TanStack Query and runs stably, a full migration has low ROI; introducing strategy hooks incrementally in new modules is more practical
  • Low-level HTTP scenarios: WebSocket connection management, streaming uploads, Server-Sent Events — these may require mixing low-level APIs with strategy hooks
  • Team unfamiliarity: the strategy paradigm requires team buy-in and understanding; teams accustomed to traditional Promise patterns face a learning curve

Conclusion

The shift from Promise to strategies represents a paradigm transition from imperative to declarative data fetching. It doesn't replace Promises — it adds a semantic abstraction layer above them for common patterns.

This trajectory mirrors the evolution in state management (Redux → Recoil / Zustand): it's not about which one is "better," but which abstraction level fits your context.

For most business applications, declaring intent in a few lines of code instead of writing boilerplate state management has concrete value. But in simple contexts, the same abstraction can add unnecessary complexity. The right choice depends on project scale, team experience, and specific requirements.

Top comments (0)