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>
);
}
This code works, but it has recurring problems:
- Boilerplate state management: loading, error, and data appear in nearly every data-fetching component
- 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
- Scattered refresh logic: triggering a refresh from outside the component (e.g., after creating a new item) requires lifting state or using refs
- 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>
);
}
Compared to the Promise version:
- Five state variables collapsed into one hook call
- No useCallback + useEffect dependency wiring
-
send(p)is semantically clearer thansetPage(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>
);
}
// 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>
);
}
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
}
);
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)