DEV Community

Cover image for Stop writing useEffect for data fetching. Use Request Strategies instead.
Scott Hu
Scott Hu

Posted on

Stop writing useEffect for data fetching. Use Request Strategies instead.

⚠️ 【Draft – Pending Review】

Stop writing useEffect for data fetching. Use Request Strategies instead.


I've been a React developer for about three years. And for most of that time, I wrote data fetching the same way everyone else did:

useEffect(() => {
  fetch('/api/users')
    .then(res => res.json())
    .then(setData)
    .catch(setError)
    .finally(() => setLoading(false));
}, []);
Enter fullscreen mode Exit fullscreen mode

It works. But it's not good.

Every list page. Every search box. Every filter. Same pattern. And the code kept growing — loading states, error handling, debounce timers, race condition flags. A simple user list with search would easily hit 80 lines of boilerplate.

Then I found alova, and I realized: I'd been fighting problems that didn't need to exist.


The Before: useEffect Hell

Here's what my code used to look like:

// 🔴 Before: 80 lines of manual state management
import { useState, useEffect, useCallback } from 'react';

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [keyword, setKeyword] = useState('');
  const [page, setPage] = useState(1);

  const fetchUsers = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const res = await fetch(`/api/users?keyword=${keyword}&page=${page}&pageSize=10`);
      const data = await res.json();
      setUsers(data.list);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }, [keyword, page]);

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

  // Manual debounce
  useEffect(() => {
    const t = setTimeout(() => fetchUsers(), 500);
    return () => clearTimeout(t);
  }, [keyword]);

  // Manual race condition handling
  useEffect(() => {
    let cancelled = false;
    fetch(`/api/users?keyword=${keyword}&page=${page}`)
      .then(res => res.json())
      .then(data => { if (!cancelled) setUsers(data.list); });
    return () => { cancelled = true; };
  }, [keyword, page]);

  if (loading) return <Spinner />;
  if (error) return <Error msg={error} />;

  return (
    <div>
      <SearchInput value={keyword} onChange={setKeyword} />
      <UserList data={users} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This code has several issues:

  1. Too many manual states — loading, error, data all declared by hand
  2. DIY debounce — setTimeout/clearTimeout everywhere, easy to mess up
  3. Race conditions — the classic cancelled flag pattern, error-prone
  4. Dependency chains — useCallback + useEffect, changing one prop means tracing the whole chain

I wrote this pattern dozens of times. Every new project, same boilerplate.


The After: Request Strategies with alova

Then I switched to alova, and the same component became this:

// 🟢 After: 15 lines with alova request strategies
import { useRequest, useWatcher } from 'alova/client';
import { alovaInstance } from './api';

function UserList() {
  const [keyword, setKeyword] = useState('');

  // List data — auto-managed loading/data/error
  const { loading, data: users = [], error } = useRequest(
    () => alovaInstance.Get('/api/users', {
      params: { page: 1, pageSize: 10 }
    }),
    { initialData: [] }
  );

  // Search — debounce + race condition handling built in
  const { data: searchResult = [] } = useWatcher(
    () => alovaInstance.Get('/api/users', {
      params: { keyword, page: 1, pageSize: 10 }
    }),
    [keyword],
    { debounce: 500 }
  );

  if (loading) return <Spinner />;
  if (error) return <Error msg={error.message} />;

  return (
    <div>
      <SearchInput value={keyword} onChange={setKeyword} />
      <UserList data={users} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This isn't magic. It's design.

alova transforms data fetching from imperative to declarative. Instead of telling the computer how to fetch, you tell it what to fetch — and the library handles the rest.

What you used to write by hand What alova does for you
3x useState for loading/data/error useRequest returns them automatically
useEffect + useCallback chains Declarative watchers with auto dependency management
setTimeout / clearTimeout for debounce debounce: 500 — one line
cancelled flag for race conditions Built-in abort on next request
Manual URL parameter construction params object, auto-serialized

What Are "Request Strategies"?

alova isn't a simple fetch wrapper. It's a strategy-based request library that covers the most common data-fetching patterns in frontend development:

Strategy Hook You tell it It handles
useRequest What to fetch loading, data, error, send, abort
useWatcher What state to watch debounce, auto-refetch, race conditions
usePagination Page config Page state, preloading, optimistic updates
useForm Form data Draft persistence, auto-submit, reset
useAutoRequest Polling config Auto-polling, focus refresh, reconnect
useCaptcha Phone number Countdown timer, auto-send
useSerialRequest Dependent calls Pass previous result to next request
useRetriableRequest Retry config Exponential backoff with jitter

Each hook maps to a pattern you've probably implemented dozens of times. The goal isn't to write less code — it's to not write it at all.


The Results

After switching to alova across my projects, here's what I've seen:

  • 60% less boilerplate — pages went from ~150 lines to ~60 lines
  • Fewer bugs — no more "forgot to clearTimeout" or "wrong cancelled flag"
  • Faster onboarding — new devs learn "use this hook for this pattern" instead of debugging useEffect chains
  • Better code review — request logic is declarative and self-documenting

Getting Started

npm install alova
Enter fullscreen mode Exit fullscreen mode

Then pick an adapter:

npm install @alova/fetch     # For browsers / Node.js
npm install alova/axios       # If you're on axios
Enter fullscreen mode Exit fullscreen mode

And write your first strategic request:

import { createAlova } from 'alova';
import fetchAdapter from '@alova/fetch';
import ReactHook from 'alova/react';

export const alovaInstance = createAlova({
  baseURL: 'https://api.example.com',
  statesHook: ReactHook,
  requestAdapter: fetchAdapter(),
  responded: res => res.json()
});

// In your component:
function Profile() {
  const { loading, data, error } = useRequest(
    () => alovaInstance.Get('/user/profile'),
    { initialData: {} }
  );
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Stop writing useEffect for data fetching. Your time is better spent on actual product logic.

Give alova a try. The docs are at alova.js.org, the source is on GitHub, and the community is welcoming.


Using alova v3. Tested in React, Vue, and Svelte.

Top comments (0)