mcpField
S
swr

swr

github.com/mindrally开发287879 次安装3 周前更新
代码仓库

关于 swr

这是一项面向 React 与 TypeScript 开发的 SWR 数据获取实践技能,涵盖缓存、重新验证、变更、分页和错误处理。

主要功能

  1. 01过期数据优先并后台更新
  2. 02自动重新验证缓存
  3. 03合并重复请求
  4. 04变更与乐观更新
  5. 05分页与无限加载

适用场景

  1. 01在 React 中获取客户端数据
  2. 02在聚焦或重连时刷新数据
  3. 03构建分页或无限滚动列表

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/mindrally/skills --skill swr

安装后在 Claude Code 中输入/swr确认已加载。

详细介绍

SWR 最佳实践

你是 SWR(stale-while-revalidate)、TypeScript 和 React 开发方面的专家。SWR 是一个用于数据获取的 React Hooks 库:它会先返回缓存中的数据(过时数据),然后发送请求(重新验证),最后提供最新数据。

核心原则

  • 使用 SWR 执行所有客户端数据获取
  • 利用自动缓存和重新验证
  • 借助 SWR 内置的状态管理减少样板代码
  • 实现完善的错误处理和加载状态
  • 使用 TypeScript 确保完整的类型安全

主要特性

  • 过时数据优先(Stale-While-Revalidate):立即返回缓存数据,然后在后台重新验证
  • 自动重新验证:在组件挂载、窗口获得焦点以及网络重新连接时触发
  • 请求去重:使用同一个键的多个组件会共享一次请求
  • 内置缓存:零配置缓存,并支持智能失效
  • 精简的 API:基于 Hook 的简单接口

项目结构

src/
  hooks/
    swr/
      useUser.ts
      usePosts.ts
      useProducts.ts
  lib/
    fetcher.ts           # Global fetcher configuration
  providers/
    SWRProvider.tsx      # SWR configuration provider
  types/
    api.ts               # API response types

设置和配置

全局配置
// providers/SWRProvider.tsx
import { SWRConfig } from 'swr';

const fetcher = async (url: string) => {
  const res = await fetch(url);

  if (!res.ok) {
    const error = new Error('An error occurred while fetching data.');
    throw error;
  }

  return res.json();
};

export function SWRProvider({ children }: { children: React.ReactNode }) {
  return (
    <SWRConfig
      value={{
        fetcher,
        revalidateOnFocus: true,
        revalidateOnReconnect: true,
        shouldRetryOnError: true,
        errorRetryCount: 3,
        dedupingInterval: 2000,
      }}
    >
      {children}
    </SWRConfig>
  );
}
自定义 Fetcher
// lib/fetcher.ts
export async function fetcher<T>(url: string): Promise<T> {
  const response = await fetch(url, {
    headers: {
      'Content-Type': 'application/json',
    },
  });

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  return response.json();
}

// With authentication
export async function authFetcher<T>(url: string): Promise<T> {
  const token = getAuthToken();

  const response = await fetch(url, {
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${token}`,
    },
  });

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  return response.json();
}

基本用法

useSWR Hook

useSWR Hook 接受三个参数:

  • key:请求的唯一字符串标识符(例如 URL)
  • fetcher:获取数据的异步函数
  • options:配置选项
import useSWR from 'swr';

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

function useUser(userId: string) {
  const { data, error, isLoading, isValidating, mutate } = useSWR<User>(
    userId ? `/api/users/${userId}` : null,
    fetcher
  );

  return {
    user: data,
    isLoading,
    isError: !!error,
    isValidating,
    mutate,
  };
}
条件式数据获取

将 null 或假值作为 key 传入,即可有条件地跳过数据获取:

// Only fetch when userId is available
const { data } = useSWR(userId ? `/api/users/${userId}` : null, fetcher);

// Using a function for dynamic keys
const { data } = useSWR(() => `/api/users/${userId}`, fetcher);

重新验证策略

自动重新验证

SWR 会在以下三种情况下自动重新验证数据:

  1. 组件已挂载(即使有缓存数据)
  2. 窗口获得焦点
  3. 浏览器重新连接网络
// Disable specific revalidation behaviors
const { data } = useSWR('/api/data', fetcher, {
  revalidateOnFocus: false,
  revalidateOnReconnect: false,
  revalidateOnMount: true,
});
手动重新验证
import { useSWRConfig } from 'swr';

function UpdateButton() {
  const { mutate } = useSWRConfig();

  const handleUpdate = async () => {
    // Revalidate specific key
    await mutate('/api/users');

    // Revalidate all keys matching a filter
    await mutate(
      (key) => typeof key === 'string' && key.startsWith('/api/users'),
      undefined,
      { revalidate: true }
    );
  };

  return <button onClick={handleUpdate}>Refresh</button>;
}
轮询 / 间隔刷新
// Refresh every 3 seconds
const { data } = useSWR('/api/realtime', fetcher, {
  refreshInterval: 3000,
});

// Conditional polling
const { data } = useSWR('/api/realtime', fetcher, {
  refreshInterval: isVisible ? 1000 : 0,
});

变更模式

乐观更新
import useSWR, { useSWRConfig } from 'swr';

function useUpdateUser() {
  const { mutate } = useSWRConfig();

  const updateUser = async (userId: string, newData: Partial<User>) => {
    // Optimistic update
    await mutate(
      `/api/users/${userId}`,
      async (currentData: User | undefined) => {
        // Update API
        const updated = await fetch(`/api/users/${userId}`, {
          method: 'PATCH',
          body: JSON.stringify(newData),
        }).then(r => r.json());

        return updated;
      },
      {
        optimisticData: (current) => ({ ...current, ...newData } as User),
        rollbackOnError: true,
        populateCache: true,
        revalidate: false,
      }
    );
  };

  return { updateUser };
}
绑定式 mutate
function UserProfile({ userId }: { userId: string }) {
  const { data: user, mutate } = useSWR(`/api/users/${userId}`, fetcher);

  const handleUpdate = async (newName: string) => {
    // Update local data immediately
    mutate({ ...user, name: newName }, false);

    // Send update to server
    await updateUserName(userId, newName);

    // Revalidate to ensure consistency
    mutate();
  };

  return (
    <div>
      <p>{user?.name}</p>
      <button onClick={() => handleUpdate('New Name')}>Update</button>
    </div>
  );
}

分页

基本分页
function usePaginatedUsers(page: number) {
  return useSWR(`/api/users?page=${page}`, fetcher, {
    keepPreviousData: true,
  });
}
使用 useSWRInfinite 实现无限加载
import useSWRInfinite from 'swr/infinite';

function useInfiniteUsers() {
  const getKey = (pageIndex: number, previousPageData: User[] | null) => {
    // Return null to stop fetching
    if (previousPageData && previousPageData.length === 0) return null;

    // Return key for next page
    return `/api/users?page=${pageIndex + 1}`;
  };

  const { data, error, size, setSize, isValidating } = useSWRInfinite(
    getKey,
    fetcher
  );

  const users = data ? data.flat() : [];
  const isLoadingMore = size > 0 && data && typeof data[size - 1] === 'undefined';
  const isEmpty = data?.[0]?.length === 0;
  const isReachingEnd = isEmpty || (data && data[data.length - 1]?.length < 10);

  return {
    users,
    isLoadingMore,
    isReachingEnd,
    loadMore: () => setSize(size + 1),
  };
}
基于游标的分页
import useSWRInfinite from 'swr/infinite';

function useCursorPagination() {
  const getKey = (pageIndex: number, previousPageData: any) => {
    if (previousPageData && !previousPageData.nextCursor) return null;

    if (pageIndex === 0) return '/api/items';

    return `/api/items?cursor=${previousPageData.nextCursor}`;
  };

  return useSWRInfinite(getKey, fetcher);
}

错误处理

组件级错误处理
function UserProfile({ userId }: { userId: string }) {
  const { data, error, isLoading } = useSWR(`/api/users/${userId}`, fetcher);

  if (isLoading) return <Skeleton />;

  if (error) {
    return (
      <ErrorMessage
        message="Failed to load user profile"
        retry={() => mutate(`/api/users/${userId}`)}
      />
    );
  }

  return <ProfileCard user={data} />;
}
全局错误处理器
<SWRConfig
  value={{
    onError: (error, key) => {
      if (error.status !== 403 && error.status !== 404) {
        // Send error to tracking service
        reportError(error, key);
      }
    },
  }}
>
  <App />
</SWRConfig>

预取

import { preload } from 'swr';

// Prefetch data before component renders
function prefetchUser(userId: string) {
  preload(`/api/users/${userId}`, fetcher);
}

// Usage: Call on hover or route change
function UserLink({ userId }: { userId: string }) {
  return (
    <Link
      to={`/users/${userId}`}
      onMouseEnter={() => prefetchUser(userId)}
    >
      View Profile
    </Link>
  );
}

键的约定

  1. 使用 SWR 执行客户端数据获取,并尽量减少配置
  2. 针对加载状态和错误消息实现条件渲染
  3. 验证数据以确保其格式符合预期,并妥善处理错误
  4. 在用户请求数据前使用预取来提升性能
  5. 利用自动重新验证确保实时数据保持最新
  6. 使用 useSWRInfinite 实现分页和无限滚动模式

与 Next.js 集成

// For server-side data, use getServerSideProps or getStaticProps
// For client-side caching and revalidation, use SWR

// pages/user/[id].tsx
export async function getStaticProps({ params }) {
  const user = await fetchUser(params.id);
  return { props: { fallback: { [`/api/users/${params.id}`]: user } } };
}

export default function UserPage({ fallback }) {
  return (
    <SWRConfig value={{ fallback }}>
      <UserProfile />
    </SWRConfig>
  );
}

应避免的反模式

  • 如果 SWR 可以处理数据获取,就不要使用 useEffect 获取数据
  • 不要忽略错误状态;始终向用户提供反馈
  • 不要忘记妥善处理加载状态
  • 如果 WebSocket 或 SSE 效率更高,就不要使用轮询
  • 不要跳过依赖数据的条件式数据获取
  • 不要忽略所获取数据的 TypeScript 类型

评论

支持 Markdown