Mastering ahooks: Practical Tips and Full Guide for React Developers

This article provides a comprehensive, step‑by‑step guide to using Alibaba's ahooks library with React 18, covering installation, core hooks like useRequest, usePagination, state management hooks, side‑effect utilities, performance optimizations, real‑world examples, common pitfalls and best‑practice recommendations.

CodeNotes
CodeNotes
CodeNotes
Mastering ahooks: Practical Tips and Full Guide for React Developers

Quick Start

Install the library: npm install ahooks ahooks supports on‑demand import and is tree‑shaking friendly, so it does not add unnecessary bundle size.

Network Requests – useRequest

Basic usage

import { useRequest } from 'ahooks';
import request from '~/req';

interface UserInfo {
  id: string;
  name: string;
  email: string;
}

const fetchUser = async (id: string): Promise<UserInfo> => {
  const res = await request.get(`/api/user/${id}`);
  return res.data;
};

const UserDetail: React.FC<{ userId: string }> = ({ userId }) => {
  const { data, loading, error } = useRequest(() => fetchUser(userId));

  if (loading) return <Spin />;
  if (error) return <div>请求失败:{error.message}</div>;

  return <div>{data?.name}</div>;
};

Manual trigger

const { loading, run } = useRequest(fetchUser, {
  manual: true,
});

<Button loading={loading} onClick={() => run('user-id-123')}>查询用户</Button>

Refresh on dependency change (refreshDeps)

const [userId, setUserId] = useState('1');
const { data } = useRequest(() => fetchUser(userId), {
  refreshDeps: [userId], // userId changes → request re‑runs
});

Polling request

const { data } = useRequest(fetchSystemStatus, {
  pollingInterval: 3000,
  pollingWhenHidden: false, // pause when page hidden
});

Debounce & throttle

// Debounce search (500 ms)
const { run } = useRequest(fetchSearchResults, {
  manual: true,
  debounceWait: 500,
});

<Input onChange={e => run(e.target.value)} />

// Throttle (once per second)
const { run } = useRequest(fetchData, {
  manual: true,
  throttleWait: 1000,
});

Cache & SWR strategy

const { data } = useRequest(fetchUser, {
  cacheKey: `user-${userId}`,
  staleTime: 5 * 60 * 1000, // 5 min cache
});

Error retry

const { data, error } = useRequest(fetchData, {
  retryCount: 3,
  retryInterval: 1000,
});

Request lifecycle callbacks

const { run } = useRequest(submitForm, {
  manual: true,
  onBefore: params => console.log('请求开始', params),
  onSuccess: (data, params) => message.success('提交成功'),
  onError: error => message.error(`提交失败:${error.message}`),
  onFinally: () => {/* always runs */},
});

Pagination – usePagination

import { usePagination } from 'ahooks';

interface PageParams { current: number; pageSize: number; }
interface UserListResult { total: number; list: UserInfo[]; }

const fetchUserList = async (params: PageParams): Promise<UserListResult> => {
  const res = await request.get('/api/users', { params });
  return res.data;
};

const UserList: React.FC = () => {
  const { data, loading, pagination } = usePagination(fetchUserList, {
    defaultPageSize: 10,
  });

  return (
    <>
      <Table
        dataSource={data?.list}
        loading={loading}
        pagination={{
          total: data?.total,
          current: pagination.current,
          pageSize: pagination.pageSize,
          onChange: pagination.onChange,
        }}
      />
    </>
  );
};

Infinite scroll – useInfiniteScroll

import { useInfiniteScroll } from 'ahooks';

interface InfiniteListResult { list: UserInfo[]; nextCursor?: string; }

const fetchInfiniteList = async (d?: InfiniteListResult): Promise<InfiniteListResult> => {
  const res = await request.get('/api/users', { params: { cursor: d?.nextCursor } });
  return res.data;
};

const InfiniteList: React.FC = () => {
  const containerRef = useRef<HTMLDivElement>(null);
  const { data, loadingMore, loadMore, noMore } = useInfiniteScroll(fetchInfiniteList, {
    target: containerRef,
    isNoMore: d => !d?.nextCursor,
  });

  return (
    <div ref={containerRef} style={{ overflow: 'auto', height: 400 }}>
      {data?.list.map(item => <div key={item.id}>{item.name}</div>)}
      {loadingMore && <Spin />}
      {noMore && <div>没有更多了</div>}
    </div>
  );
};

State Management Hooks

useToggle – boolean toggle

import { useToggle } from 'ahooks';

const Modal: React.FC = () => {
  const [visible, { toggle, setLeft: hide, setRight: show }] = useToggle(false);
  return (
    <>
      <Button onClick={show}>打开弹窗</Button>
      <Dialog open={visible} onClose={hide}>内容</Dialog>
    </>
  );
};

useBoolean – semantic boolean

import { useBoolean } from 'ahooks';

const [loading, { setTrue: startLoading, setFalse: stopLoading }] = useBoolean(false);

const handleSubmit = async () => {
  startLoading();
  try { await submitData(); } finally { stopLoading(); }
};

useSet & useMap – collections

import { useSet, useMap } from 'ahooks';

// useSet
const [selectedIds, { add, remove, has, reset }] = useSet<string>();
const toggleRow = (id: string) => {
  has(id) ? remove(id) : add(id);
};

// useMap
const [cache, { set: setCache, get: getCache, remove: removeCache }] = useMap<string, any>();

useLocalStorageState – persistence

import { useLocalStorageState } from 'ahooks';

const [theme, setTheme] = useLocalStorageState<'light' | 'dark'>('app-theme', {
  defaultValue: 'light',
  serializer: JSON.stringify,
  deserializer: JSON.parse,
});

useUrlState – sync with URL

import { useUrlState } from 'ahooks';

const [state, setState] = useUrlState({ page: '1', keyword: '', status: 'active' });
// URL becomes ?page=1&keyword=&status=active
setState({ page: '2', keyword: 'john' });

Side‑Effect Hooks

useDebounceEffect – debounced side effect

import { useDebounceEffect } from 'ahooks';

useDebounceEffect(() => {
  if (keyword) fetchSearchResults(keyword);
}, [keyword], { wait: 500 });

useThrottleEffect – throttled side effect

import { useThrottleEffect } from 'ahooks';

useThrottleEffect(() => {
  console.log('scrollTop:', window.scrollY);
}, [scrollY], { wait: 200 });

useDeepCompareEffect – deep compare deps

import { useDeepCompareEffect } from 'ahooks';

useDeepCompareEffect(() => {
  fetchData(filters);
}, [filters]);

useUpdateEffect – skip first run

import { useUpdateEffect } from 'ahooks';

useUpdateEffect(() => {
  message.info('筛选条件已更新');
}, [filters]);

useAsyncEffect – async side effect

import { useAsyncEffect } from 'ahooks';

useAsyncEffect(async () => {
  const data = await fetchInitData();
  setInitData(data);
}, []);

Performance Optimization Hooks

useDebounceFn & useThrottleFn – debounced / throttled functions

import { useDebounceFn, useThrottleFn } from 'ahooks';

const { run: debouncedSearch } = useDebounceFn((keyword: string) => {
  fetchSearchResults(keyword);
}, { wait: 300 });

const { run: throttledScroll } = useThrottleFn(() => {
  updateScrollPosition();
}, { wait: 100 });

useDebounceValue & useThrottleValue – debounced / throttled values

import { useDebounceValue, useThrottleValue } from 'ahooks';

const [inputValue, setInputValue] = useState('');
const debouncedValue = useDebounceValue(inputValue, { wait: 500 });

useEffect(() => {
  if (debouncedValue) fetchResults(debouncedValue);
}, [debouncedValue]);

useMemoizedFn – stable function reference

import { useMemoizedFn } from 'ahooks';

const handleSubmit = useMemoizedFn(async (values: FormValues) => {
  const result = await submitData({ ...values, userId });
  onSuccess(result);
});

<MemoizedForm onSubmit={handleSubmit} />

DOM Hooks

useEventListener – event binding

import { useEventListener } from 'ahooks';

useEventListener('keydown', e => {
  if (e.key === 'Escape') closeModal();
});

const buttonRef = useRef<HTMLButtonElement>(null);
useEventListener('click', handleClick, { target: buttonRef });

useInViewport – visibility detection

import { useInViewport } from 'ahooks';

const ref = useRef<HTMLDivElement>(null);
const [inViewport] = useInViewport(ref);

return (
  <div ref={ref}>
    {inViewport ? <img src={imageSrc} alt="" /> : <Skeleton />}
  </div>
);

useSize – element size

import { useSize } from 'ahooks';

const containerRef = useRef<HTMLDivElement>(null);
const size = useSize(containerRef);
const columns = size?.width && size.width > 800 ? 3 : 1;

useScroll – scroll position

import { useScroll } from 'ahooks';

const position = useScroll(document);
const showBackTop = (position?.top ?? 0) > 300;

useTextSelection – text selection

import { useTextSelection } from 'ahooks';

const { text, left, top } = useTextSelection();

return (
  <>
    <article>长文章内容...</article>
    {text && (
      <div style={{ position: 'fixed', left, top }}>
        <Button onClick={() => translate(text)}>翻译</Button>
        <Button onClick={() => copy(text)}>复制</Button>
      </div>
    )}
  </>
);

useFullscreen – full‑screen control

import { useFullscreen } from 'ahooks';

const videoRef = useRef<HTMLDivElement>(null);
const [isFullscreen, { enterFullscreen, exitFullscreen, toggleFullscreen }] =
  useFullscreen(videoRef);

<Button onClick={toggleFullscreen}>
  {isFullscreen ? '退出全屏' : '全屏'}
</Button>

useDrop & useDrag – drag‑and‑drop

import { useDrop, useDrag } from 'ahooks';

const DropZone: React.FC = () => {
  const [isHovering, setIsHovering] = useState(false);
  const [props] = useDrop({
    onFiles: files => uploadFiles(files),
    onText: text => console.log('拖入文本:', text),
    onDragEnter: () => setIsHovering(true),
    onDragLeave: () => setIsHovering(false),
  });

  return (
    <div {...props} style={{ border: isHovering ? '2px dashed blue' : '2px dashed gray' }}>
      拖拽文件到此处
    </div>
  );
};

Timer Hooks

useInterval – recurring timer

import { useInterval } from 'ahooks';

const [count, setCount] = useState(0);
useInterval(() => setCount(c => c + 1), 1000);

useTimeout – delayed execution

import { useTimeout } from 'ahooks';

useTimeout(() => setVisible(false), 3000);

useCountDown – countdown

import { useCountDown } from 'ahooks';

const [countdown, formattedRes] = useCountDown({ targetDate: targetTime });
const { seconds } = formattedRes;

<Button disabled={seconds > 0} onClick={sendCode}>
  {seconds > 0 ? `${seconds}s 后重发` : '发送验证码'}
</Button>

Practical Cases

Case 1 – Debounced search table

import { useRequest, useDebounceFn, usePagination } from 'ahooks';
import { useState } from 'react';

interface SearchParams { keyword: string; current: number; pageSize: number; }

const fetchTableData = async (params: SearchParams) => {
  const res = await request.get('/api/table', { params });
  return res.data;
};

const DataTable: React.FC = () => {
  const [keyword, setKeyword] = useState('');
  const [searchKeyword, setSearchKeyword] = useState('');

  const { run: debouncedSetKeyword } = useDebounceFn(
    (value: string) => setSearchKeyword(value),
    { wait: 500 }
  );

  const { data, loading, pagination } = usePagination(
    ({ current, pageSize }) => fetchTableData({ keyword: searchKeyword, current, pageSize }),
    { refreshDeps: [searchKeyword] }
  );

  return (
    <>
      <Input
        value={keyword}
        onChange={e => { setKeyword(e.target.value); debouncedSetKeyword(e.target.value); }}
        placeholder="搜索..."
      />
      <Table
        dataSource={data?.list}
        loading={loading}
        pagination={{
          total: data?.total,
          current: pagination.current,
          pageSize: pagination.pageSize,
          onChange: pagination.onChange,
        }}
      />
    </>
  );
};

Case 2 – Offline‑aware data sync

import { useNetwork, useRequest, useInterval } from 'ahooks';

const SyncStatus: React.FC = () => {
  const { online } = useNetwork();
  const [lastSynced, setLastSynced] = useState<Date | null>(null);

  const { run: syncData } = useRequest(syncToServer, {
    manual: true,
    onSuccess: () => setLastSynced(new Date()),
  });

  useInterval(() => { if (online) syncData(); }, online ? 30000 : null);

  return (
    <div>
      {online ? '🟢 在线' : '🔴 离线'}
      {lastSynced && ` · 上次同步:${lastSynced.toLocaleTimeString()}`}
    </div>
  );
};

Case 3 – Undoable rich‑text editor

import { useHistoryTravel, useKeyPress, useMemoizedFn } from 'ahooks';

const RichEditor: React.FC = () => {
  const { value, setValue, back, forward, backLength, forwardLength } =
    useHistoryTravel<string>('');

  const handleChange = useMemoizedFn((content: string) => setValue(content));

  useKeyPress(['ctrl.z', 'meta.z'], e => { e.preventDefault(); if (backLength > 0) back(); });
  useKeyPress(['ctrl.shift.z', 'meta.shift.z'], e => { e.preventDefault(); if (forwardLength > 0) forward(); });

  return (
    <div>
      <Toolbar>
        <Button disabled={backLength === 0} onClick={() => back()}>撤销 ({backLength})</Button>
        <Button disabled={forwardLength === 0} onClick={() => forward()}>重做 ({forwardLength})</Button>
      </Toolbar>
      <TextArea value={value} onChange={e => handleChange(e.target.value)} />
    </div>
  );
};

Case 4 – Virtual list with infinite scroll

import { useInfiniteScroll, useInViewport } from 'ahooks';
import { useRef } from 'react';

const fetchPage = async (d?: { nextCursor: string; list: Post[] }) => {
  const res = await request.get('/api/posts', { params: { cursor: d?.nextCursor } });
  return res.data as { list: Post[]; nextCursor: string };
};

const Feed: React.FC = () => {
  const listRef = useRef<HTMLDivElement>(null);
  const footerRef = useRef<HTMLDivElement>(null);
  const [footerVisible] = useInViewport(footerRef);

  const { data, loadingMore, loadMore, noMore } = useInfiniteScroll(fetchPage, {
    target: listRef,
    isNoMore: d => !d?.nextCursor,
  });

  useEffect(() => {
    if (footerVisible && !loadingMore && !noMore) loadMore();
  }, [footerVisible]);

  return (
    <div ref={listRef} style={{ height: '100vh', overflow: 'auto' }}>
      {data?.list.map(post => <PostCard key={post.id} post={post} />)}
      <div ref={footerRef}>
        {loadingMore && <Spin />}
        {noMore && <div>已经到底了</div>}
      </div>
    </div>
  );
};

Common Pitfalls & Best Practices

1️⃣ Keep the service function stable

// ❌ Bad: creates a new function each render → endless loop
const { data } = useRequest(async () => {
  return request.get(`/api/user/${userId}`);
});

// ✅ Good: pass dependencies via refreshDeps
const { data } = useRequest(
  () => request.get(`/api/user/${userId}`).then(r => r.data),
  { refreshDeps: [userId] }
);

2️⃣ Prefer useMemoizedFn over useCallback

// ❌ useCallback needs a long dependency list
const handleClick = useCallback(() => doSomething(a, b, c), [a, b, c]);

// ✅ useMemoizedFn gives a stable reference and always reads latest values
const handleClick = useMemoizedFn(() => doSomething(a, b, c));

3️⃣ Serialize complex objects with useLocalStorageState

const [config, setConfig] = useLocalStorageState<AppConfig>('app-config', {
  defaultValue: DEFAULT_CONFIG,
  serializer: v => JSON.stringify(v ?? null),
  deserializer: v => {
    try { return JSON.parse(v) ?? DEFAULT_CONFIG; }
    catch { return DEFAULT_CONFIG; }
  },
});

4️⃣ Fix target time for useCountDown

// ❌ Re‑calculating targetDate each render resets the timer
const [cd] = useCountDown({ targetDate: Date.now() + 60000 });

// ✅ Store the target time in a ref or state
const [targetTime] = useState(() => Date.now() + 60000);
const [cd] = useCountDown({ targetDate: targetTime });

5️⃣ Combine pollingInterval with page visibility

const { data } = useRequest(fetchLiveData, {
  pollingInterval: 5000,
  pollingWhenHidden: false, // pause when tab hidden
  pollingErrorRetryCount: 3, // stop after 3 consecutive errors
});

Conclusion

ahooks follows a design philosophy of "out‑of‑the‑box usability and business‑logic focus". By leveraging the hooks shown above, developers can dramatically reduce boilerplate, keep component logic clear, and enjoy type safety and performance guarantees. All examples assume ahooks 3.x with TypeScript.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

FrontendperformanceTypeScriptState ManagementBest PracticesReact HooksuseRequestahooks
CodeNotes
Written by

CodeNotes

Discuss code and AI, and document daily life and personal growth.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.