diff --git a/README.md b/README.md
index 805b6eb1c9..43c933cf6c 100644
--- a/README.md
+++ b/README.md
@@ -86,7 +86,7 @@ library to handle that part.
This library is created by the team behind [Next.js](https://nextjs.org), with contributions from our community:
- Shu Ding ([@shuding\_](https://x.com/shuding_)) - [Vercel](https://vercel.com)
-- Jiachi Liu ([@huozhi])(https://x.com/huozhi)) - [Vercel](https://vercel.com)
+- Jiachi Liu ([@huozhi](https://x.com/huozhi)) - [Vercel](https://vercel.com)
- Guillermo Rauch ([@rauchg](https://x.com/rauchg)) - [Vercel](https://vercel.com)
- Joe Haddad ([@timer150](https://x.com/timer150)) - [Vercel](https://vercel.com)
- Paco Coursey ([@pacocoursey](https://x.com/pacocoursey)) - [Vercel](https://vercel.com)
diff --git a/e2e/site/app/perf/page.tsx b/e2e/site/app/perf/page.tsx
new file mode 100644
index 0000000000..e536278969
--- /dev/null
+++ b/e2e/site/app/perf/page.tsx
@@ -0,0 +1,63 @@
+'use client'
+import { useState } from 'react'
+import useSWR from 'swr'
+
+const elementCount = 10_000
+const useData = () => {
+ return useSWR('1', async (url: string) => {
+ return 1
+ })
+}
+
+const HookUser = () => {
+ const { data } = useData()
+ return
{data}
+}
+/**
+ * This renders 10,000 divs and is used to compare against the render performance
+ * when using swr.
+ */
+const CheapComponent = () => {
+ const cheapComponents = Array.from({ length: elementCount }, (_, i) => (
+ {i}
+ ))
+ return (
+
+
Cheap Component
+ {cheapComponents}
+
+ )
+}
+
+/**
+ * This renders 10,000 divs, each of which uses the same swr hook.
+ */
+const ExpensiveComponent = () => {
+ const hookComponents = Array.from({ length: elementCount }, (_, i) => (
+
+ ))
+ return (
+
+
Expensive Component
+ {hookComponents}
+
+ )
+}
+
+export default function PerformancePage() {
+ const [renderExpensive, setRenderExpensive] = useState(false)
+ return (
+
+
Performance Page
+
+ setRenderExpensive(e.target.checked)}
+ />
+ Render with swr
+
+ {!renderExpensive ? : }
+
+ )
+}
diff --git a/e2e/test/perf.test.ts b/e2e/test/perf.test.ts
new file mode 100644
index 0000000000..38baddf90c
--- /dev/null
+++ b/e2e/test/perf.test.ts
@@ -0,0 +1,115 @@
+import { test, expect } from '@playwright/test'
+
+test.describe('performance', () => {
+ test('should render expensive component within 1 second after checkbox click', async ({
+ page
+ }) => {
+ // Navigate to the perf page
+ await page.goto('./perf', { waitUntil: 'load' })
+
+ // Inject performance measurement into the page
+ await page.evaluate(() => {
+ const checkboxInput = document.querySelector(
+ 'input[type="checkbox"]'
+ ) as HTMLInputElement
+ let expensiveComponentContainer: HTMLElement | null = null
+ const targetChildCount = 10_000
+ let startTime = 0
+
+ // Track when React starts and completes rendering
+ const observer = new MutationObserver(mutations => {
+ for (const mutation of mutations) {
+ const addedNodes = Array.from(mutation.addedNodes)
+ for (const node of addedNodes) {
+ if (node instanceof HTMLElement) {
+ const h2 = node.querySelector?.('h2')
+ if (h2?.textContent === 'Expensive Component') {
+ expensiveComponentContainer = node
+ console.log('Found Expensive Component container')
+ }
+ }
+ }
+
+ if (expensiveComponentContainer) {
+ const renderedComponents =
+ expensiveComponentContainer.querySelectorAll('div > div').length
+
+ if (renderedComponents % 1000 === 0 && renderedComponents > 0) {
+ console.log(`Rendered ${renderedComponents} components...`)
+ }
+
+ if (renderedComponents >= targetChildCount) {
+ console.log(`All ${renderedComponents} components rendered!`)
+
+ // Use requestAnimationFrame to ensure the browser has painted
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ // Double RAF to ensure we're after the paint
+ window.performance.mark('expensive-component-painted')
+ const paintTime = performance.now() - startTime
+ console.log(
+ `Total time including paint: ${paintTime.toFixed(2)}ms`
+ )
+ })
+ })
+
+ observer.disconnect()
+ }
+ }
+ }
+ })
+
+ observer.observe(document.body, { childList: true, subtree: true })
+
+ // Capture more precise timing
+ checkboxInput.addEventListener(
+ 'click',
+ () => {
+ startTime = performance.now()
+ window.performance.mark('state-change-start')
+ console.log('Checkbox clicked, state change started')
+ },
+ { once: true }
+ )
+ })
+
+ // Find and click the checkbox
+ const checkbox = page.locator('input[type="checkbox"]')
+ await checkbox.click()
+
+ // Wait for the expensive component to be fully rendered
+ const expensiveComponentHeading = page.locator(
+ 'h2:has-text("Expensive Component")'
+ )
+ await expect(expensiveComponentHeading).toBeVisible({ timeout: 60_000 })
+
+ // Wait for all components and paint to complete
+ await page.waitForFunction(
+ () => {
+ return (
+ window.performance.getEntriesByName('expensive-component-painted')
+ .length > 0
+ )
+ },
+ { timeout: 5000 }
+ )
+
+ // Get the render time from state change to paint
+ const renderTime = await page.evaluate(() => {
+ const startMark =
+ window.performance.getEntriesByName('state-change-start')[0]
+ const paintMark = window.performance.getEntriesByName(
+ 'expensive-component-painted'
+ )[0]
+
+ if (!startMark || !paintMark) {
+ throw new Error('Performance marks not found')
+ }
+
+ return paintMark.startTime - startMark.startTime
+ })
+
+ // Assert that the rendering took less than 1 second (1000ms)
+ expect(renderTime).toBeLessThan(1000)
+ })
+})
diff --git a/examples/suspense-global/README.md b/examples/suspense-global/README.md
new file mode 100644
index 0000000000..a5f4641f8d
--- /dev/null
+++ b/examples/suspense-global/README.md
@@ -0,0 +1,30 @@
+# Basic
+
+## One-Click Deploy
+
+Deploy your own SWR project with Vercel.
+
+[](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/suspense)
+
+## How to Use
+
+Download the example:
+
+```bash
+curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/suspense
+cd suspense
+```
+
+Install it and run:
+
+```bash
+yarn
+yarn dev
+# or
+npm install
+npm run dev
+```
+
+## The Idea behind the Example
+
+Show how to use the SWR suspense option with React suspense.
diff --git a/examples/suspense-global/components/error-handling.ts b/examples/suspense-global/components/error-handling.ts
new file mode 100644
index 0000000000..f604ae0dd1
--- /dev/null
+++ b/examples/suspense-global/components/error-handling.ts
@@ -0,0 +1,17 @@
+import React from 'react'
+
+export default class ErrorBoundary extends React.Component {
+ state = { hasError: false, error: null }
+ static getDerivedStateFromError(error: any) {
+ return {
+ hasError: true,
+ error
+ }
+ }
+ render() {
+ if (this.state.hasError) {
+ return this.props.fallback
+ }
+ return this.props.children
+ }
+}
diff --git a/examples/suspense-global/global-swr-config.tsx b/examples/suspense-global/global-swr-config.tsx
new file mode 100644
index 0000000000..a8a8f89d23
--- /dev/null
+++ b/examples/suspense-global/global-swr-config.tsx
@@ -0,0 +1,24 @@
+'use client'
+
+import { SWRConfig } from 'swr'
+
+import fetcher from './libs/fetch'
+
+declare module 'swr' {
+ interface SWRGlobalConfig {
+ suspense: true
+ }
+}
+
+export function GlobalSWRConfig({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/examples/suspense-global/libs/fetch.ts b/examples/suspense-global/libs/fetch.ts
new file mode 100644
index 0000000000..86f1252fc5
--- /dev/null
+++ b/examples/suspense-global/libs/fetch.ts
@@ -0,0 +1,8 @@
+export default async function fetcher(...args: [any]) {
+ const res = await fetch(...args)
+ if (!res.ok) {
+ throw new Error('An error occurred while fetching the data.')
+ } else {
+ return res.json()
+ }
+}
diff --git a/examples/suspense-global/next-env.d.ts b/examples/suspense-global/next-env.d.ts
new file mode 100644
index 0000000000..a4a7b3f5cf
--- /dev/null
+++ b/examples/suspense-global/next-env.d.ts
@@ -0,0 +1,5 @@
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
diff --git a/examples/suspense-global/package.json b/examples/suspense-global/package.json
new file mode 100644
index 0000000000..f2671e2100
--- /dev/null
+++ b/examples/suspense-global/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "suspense-global",
+ "version": "1.0.0",
+ "main": "index.js",
+ "license": "MIT",
+ "dependencies": {
+ "next": "latest",
+ "react": "latest",
+ "react-dom": "latest",
+ "swr": "latest"
+ },
+ "scripts": {
+ "dev": "next",
+ "start": "next start",
+ "build": "next build"
+ }
+}
diff --git a/examples/suspense-global/pages/[user]/[repo].tsx b/examples/suspense-global/pages/[user]/[repo].tsx
new file mode 100644
index 0000000000..7e2fa7d85b
--- /dev/null
+++ b/examples/suspense-global/pages/[user]/[repo].tsx
@@ -0,0 +1,30 @@
+import dynamic from 'next/dynamic'
+import Link from 'next/link'
+import { Suspense } from 'react'
+import ErrorHandling from '../../components/error-handling'
+import { useRouter } from 'next/router'
+
+const Detail = dynamic(() => import('./detail'), {
+ ssr: false
+})
+
+export default function Repo() {
+ const router = useRouter()
+ if (!router.isReady) return null
+ const { user, repo } = router.query
+ const id = `${user}/${repo}`
+
+ return (
+
+
{id}
+ loading... }>
+ oooops!}>
+
+
+
+
+
+ Back
+
+ )
+}
diff --git a/examples/suspense-global/pages/[user]/detail.tsx b/examples/suspense-global/pages/[user]/detail.tsx
new file mode 100644
index 0000000000..5201fc2b94
--- /dev/null
+++ b/examples/suspense-global/pages/[user]/detail.tsx
@@ -0,0 +1,20 @@
+import useSWR from 'swr'
+import { RepoData } from '../api/data'
+
+const Detail = ({ id }: { id: string }) => {
+ const { data } = useSWR('/api/data?id=' + id)
+
+ return (
+ <>
+ {data ? (
+
+
forks: {data.forks_count}
+
stars: {data.stargazers_count}
+
watchers: {data.watchers}
+
+ ) : null}
+ >
+ )
+}
+
+export default Detail
diff --git a/examples/suspense-global/pages/_app.tsx b/examples/suspense-global/pages/_app.tsx
new file mode 100644
index 0000000000..0aae4cb910
--- /dev/null
+++ b/examples/suspense-global/pages/_app.tsx
@@ -0,0 +1,10 @@
+import type { AppProps } from 'next/app'
+import { GlobalSWRConfig } from 'global-swr-config'
+
+export default function MyApp({ Component, pageProps }: AppProps) {
+ return (
+
+
+
+ )
+}
diff --git a/examples/suspense-global/pages/api/data.ts b/examples/suspense-global/pages/api/data.ts
new file mode 100644
index 0000000000..456a4cc59d
--- /dev/null
+++ b/examples/suspense-global/pages/api/data.ts
@@ -0,0 +1,40 @@
+import { NextApiRequest, NextApiResponse } from 'next'
+
+const projects = [
+ 'facebook/flipper',
+ 'vuejs/vuepress',
+ 'rust-lang/rust',
+ 'vercel/next.js',
+ 'emperor/clothes'
+] as const
+
+export type ProjectsData = typeof projects
+
+export interface RepoData {
+ forks_count: number
+ stargazers_count: number
+ watchers: number
+}
+
+export default function api(req: NextApiRequest, res: NextApiResponse) {
+ if (req.query.id) {
+ if (req.query.id === projects[4]) {
+ setTimeout(() => {
+ res.status(404).json({ msg: 'not found' })
+ })
+ } else {
+ // a slow endpoint for getting repo data
+ fetch(`https://api.github.com/repos/${req.query.id}`)
+ .then(res => res.json())
+ .then(data => {
+ setTimeout(() => {
+ res.json(data)
+ }, 2000)
+ })
+ }
+ } else {
+ setTimeout(() => {
+ res.json(projects)
+ }, 2000)
+ }
+}
diff --git a/examples/suspense-global/pages/index.tsx b/examples/suspense-global/pages/index.tsx
new file mode 100644
index 0000000000..2da57a6885
--- /dev/null
+++ b/examples/suspense-global/pages/index.tsx
@@ -0,0 +1,17 @@
+import { Suspense } from 'react'
+import dynamic from 'next/dynamic'
+
+const Repos = dynamic(() => import('./repos'), {
+ ssr: false
+})
+
+export default function Index() {
+ return (
+
+
Trending Projects
+ loading... }>
+
+
+
+ )
+}
diff --git a/examples/suspense-global/pages/repos.tsx b/examples/suspense-global/pages/repos.tsx
new file mode 100644
index 0000000000..c6a04e121f
--- /dev/null
+++ b/examples/suspense-global/pages/repos.tsx
@@ -0,0 +1,22 @@
+import Link from 'next/link'
+import { ProjectsData } from './api/data'
+
+import useSWR from 'swr'
+
+const Repos = () => {
+ const { data } = useSWR('/api/data')
+
+ return (
+ <>
+ {data.map(project => (
+
+
+ {project}
+
+
+ ))}
+ >
+ )
+}
+
+export default Repos
diff --git a/examples/suspense-global/tsconfig.json b/examples/suspense-global/tsconfig.json
new file mode 100644
index 0000000000..da520dfb18
--- /dev/null
+++ b/examples/suspense-global/tsconfig.json
@@ -0,0 +1,30 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "noImplicitAny": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "baseUrl": ".",
+ "paths": {
+ "~/*": ["./*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/package.json b/package.json
index 1adf11b6e1..0184b6c5ca 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "swr",
- "version": "2.3.3",
+ "version": "2.3.4",
"description": "React Hooks library for remote data fetching",
"keywords": [
"swr",
@@ -108,7 +108,7 @@
"lint": "eslint . --ext .ts,.tsx --cache",
"lint:fix": "pnpm lint --fix",
"coverage": "jest --coverage",
- "test-typing": "tsc --noEmit -p test/type/tsconfig.json && tsc --noEmit -p test/tsconfig.json",
+ "test-typing": "tsc -p test/tsconfig.json && tsc -p test/type/tsconfig.json && tsc -p test/type/suspense/tsconfig.json",
"test": "jest",
"test:build": "jest --config jest.config.build.js",
"test:e2e": "playwright test",
diff --git a/src/_internal/types.ts b/src/_internal/types.ts
index 528dae9327..c3cb0dd227 100644
--- a/src/_internal/types.ts
+++ b/src/_internal/types.ts
@@ -1,3 +1,4 @@
+import type { SWRGlobalConfig } from '../index'
import type * as revalidateEvents from './events'
export type GlobalState = [
@@ -33,7 +34,9 @@ export type ReactUsePromise = Promise & {
export type BlockingData<
Data = any,
Options = SWROptions
-> = Options extends undefined
+> = SWRGlobalConfig extends { suspense: true }
+ ? true
+ : Options extends undefined
? false
: Options extends { suspense: true }
? true
@@ -456,7 +459,11 @@ export type SWRConfiguration<
export type IsLoadingResponse<
Data = any,
Options = SWROptions
-> = Options extends { suspense: true } ? false : boolean
+> = SWRGlobalConfig extends { suspense: true }
+ ? Options extends { suspense: true }
+ ? false
+ : false
+ : boolean
type SWROptions = SWRConfiguration>
type SWRConfigurationWithOptionalFallback =
diff --git a/src/_internal/utils/hash.ts b/src/_internal/utils/hash.ts
index c9528a6a84..7c992ca6ce 100644
--- a/src/_internal/utils/hash.ts
+++ b/src/_internal/utils/hash.ts
@@ -6,8 +6,10 @@ import { OBJECT, isUndefined } from './shared'
// complexity is almost O(1).
const table = new WeakMap()
-const isObjectType = (value: any, type: string) =>
- OBJECT.prototype.toString.call(value) === `[object ${type}]`
+const getTypeName = (value: any) => OBJECT.prototype.toString.call(value)
+
+const isObjectTypeName = (typeName: string, type: string) =>
+ typeName === `[object ${type}]`
// counter of the key
let counter = 0
@@ -22,9 +24,10 @@ let counter = 0
// parsable.
export const stableHash = (arg: any): string => {
const type = typeof arg
- const isDate = isObjectType(arg, 'Date')
- const isRegex = isObjectType(arg, 'RegExp')
- const isPlainObject = isObjectType(arg, 'Object')
+ const typeName = getTypeName(arg)
+ const isDate = isObjectTypeName(typeName, 'Date')
+ const isRegex = isObjectTypeName(typeName, 'RegExp')
+ const isPlainObject = isObjectTypeName(typeName, 'Object')
let result: any
let index: any
diff --git a/src/_internal/utils/mutate.ts b/src/_internal/utils/mutate.ts
index 6ea91bec08..071af7803e 100644
--- a/src/_internal/utils/mutate.ts
+++ b/src/_internal/utils/mutate.ts
@@ -123,6 +123,7 @@ export async function internalMutate(
let data: any = _data
let error: unknown
+ let isError = false
// Update global timestamps.
const beforeMutationTs = getTimestamp()
@@ -155,6 +156,7 @@ export async function internalMutate(
} catch (err) {
// If it throws an error synchronously, we shouldn't update the cache.
error = err
+ isError = true
}
}
@@ -164,15 +166,16 @@ export async function internalMutate(
// avoid race conditions.
data = await (data as Promise).catch(err => {
error = err
+ isError = true
})
// Check if other mutations have occurred since we've started this mutation.
// If there's a race we don't update cache or broadcast the change,
// just return the data.
if (beforeMutationTs !== MUTATION[key][0]) {
- if (error) throw error
+ if (isError) throw error
return data
- } else if (error && hasOptimisticData && rollbackOnError(error)) {
+ } else if (isError && hasOptimisticData && rollbackOnError(error)) {
// Rollback. Always populate the cache in this case but without
// transforming the data.
populateCache = true
@@ -184,7 +187,7 @@ export async function internalMutate(
// If we should write back the cache after request.
if (populateCache) {
- if (!error) {
+ if (!isError) {
// Transform the result into data.
if (isFunction(populateCache)) {
const populateCachedData = populateCache(data, committedData)
@@ -207,7 +210,7 @@ export async function internalMutate(
})
// Throw error or return data
- if (error) {
+ if (isError) {
if (throwOnError) throw error
return
}
diff --git a/src/index/index.ts b/src/index/index.ts
index b5419ddb53..2ae26b1437 100644
--- a/src/index/index.ts
+++ b/src/index/index.ts
@@ -8,6 +8,12 @@ export { useSWRConfig } from '../_internal'
export { mutate } from '../_internal'
export { preload } from '../_internal'
+// Config
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface SWRGlobalConfig {
+ // suspense: true
+}
+
// Types
export type {
SWRConfiguration,
diff --git a/src/index/use-swr.ts b/src/index/use-swr.ts
index 21c42a9c2e..91f3a471a9 100644
--- a/src/index/use-swr.ts
+++ b/src/index/use-swr.ts
@@ -278,8 +278,8 @@ export const useSWRHandler = (
const returnedData = keepPreviousData
? isUndefined(cachedData)
- // checking undefined to avoid null being fallback as well
- ? isUndefined(laggyDataRef.current)
+ ? // checking undefined to avoid null being fallback as well
+ isUndefined(laggyDataRef.current)
? data
: laggyDataRef.current
: cachedData
@@ -639,13 +639,17 @@ export const useSWRHandler = (
// Trigger a revalidation
if (shouldDoInitialRevalidation) {
- if (isUndefined(data) || IS_SERVER) {
- // Revalidate immediately.
- softRevalidate()
- } else {
- // Delay the revalidate if we have data to return so we won't block
- // rendering.
- rAF(softRevalidate)
+ // Performance optimization: if a request is already in progress for this key,
+ // skip the revalidation to avoid redundant work
+ if (!FETCH[key]) {
+ if (isUndefined(data) || IS_SERVER) {
+ // Revalidate immediately.
+ softRevalidate()
+ } else {
+ // Delay the revalidate if we have data to return so we won't block
+ // rendering.
+ rAF(softRevalidate)
+ }
}
}
diff --git a/test/tsconfig.json b/test/tsconfig.json
index ba0b1f7a89..4cf0256290 100644
--- a/test/tsconfig.json
+++ b/test/tsconfig.json
@@ -1,6 +1,7 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
+ "noEmit": true,
"strict": false,
"jsx": "react-jsx",
"baseUrl": "..",
@@ -10,8 +11,8 @@
"swr/immutable": ["./immutable/src/index.ts"],
"swr/mutation": ["./mutation/src/index.ts"],
"swr/_internal": ["./_internal/src/index.ts"],
- "swr/subscription": ["subscription/src/index.ts"],
- },
+ "swr/subscription": ["subscription/src/index.ts"]
+ }
},
"include": [".", "./jest-setup.ts"],
"exclude": ["./type"]
diff --git a/test/type/suspense/helper-types.tsx b/test/type/suspense/helper-types.tsx
new file mode 100644
index 0000000000..0d571f58e6
--- /dev/null
+++ b/test/type/suspense/helper-types.tsx
@@ -0,0 +1,19 @@
+import type { BlockingData } from 'swr/_internal'
+import { expectType } from '../utils'
+
+declare module 'swr' {
+ interface SWRGlobalConfig {
+ suspense: true
+ }
+}
+
+export function testDataCached() {
+ expectType>(true)
+ expectType>(true)
+ expectType<
+ BlockingData
+ >(true)
+ expectType>(
+ true
+ )
+}
diff --git a/test/type/suspense/suspense.ts b/test/type/suspense/suspense.ts
new file mode 100644
index 0000000000..58323b9573
--- /dev/null
+++ b/test/type/suspense/suspense.ts
@@ -0,0 +1,13 @@
+import useSWR from 'swr'
+import { expectType } from '../utils'
+
+declare module 'swr' {
+ interface SWRGlobalConfig {
+ suspense: true
+ }
+}
+
+export function testSuspense() {
+ const { data } = useSWR('/api', (k: string) => Promise.resolve(k))
+ expectType(data)
+}
diff --git a/test/type/suspense/tsconfig.json b/test/type/suspense/tsconfig.json
new file mode 100644
index 0000000000..706abd8cd2
--- /dev/null
+++ b/test/type/suspense/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "noEmit": true,
+ "strict": true,
+ "jsx": "react-jsx"
+ },
+ "include": ["./**/*.ts", "./**/*.tsx"],
+ "exclude": []
+}
diff --git a/test/type/tsconfig.json b/test/type/tsconfig.json
index 76044134d6..ed5b135090 100644
--- a/test/type/tsconfig.json
+++ b/test/type/tsconfig.json
@@ -1,8 +1,10 @@
{
"extends": "../../tsconfig.json",
- "compilerOptions": {
+ "compilerOptions": {
+ "noEmit": true,
"strict": true,
- "jsx": "react-jsx",
+ "jsx": "react-jsx"
},
"include": ["./**/*.ts", "./**/*.tsx"],
+ "exclude": ["./suspense"]
}
diff --git a/test/use-swr-remote-mutation.test.tsx b/test/use-swr-remote-mutation.test.tsx
index bcb7074e34..955d2b9b24 100644
--- a/test/use-swr-remote-mutation.test.tsx
+++ b/test/use-swr-remote-mutation.test.tsx
@@ -1112,4 +1112,32 @@ describe('useSWR - remote mutation', () => {
expect(error.message).toBe('Can’t trigger the mutation: missing key.')
})
+
+ it('should call `onError` and `onRejected` but do not call `onSuccess` if value an error is cast to false', async () => {
+ const key = createKey()
+ const onSuccess = jest.fn()
+ const onError = jest.fn()
+ const onRejected = jest.fn()
+
+ const fetcher = () => {
+ return new Promise((_, reject) => reject(''));
+ };
+
+ function Page() {
+ const { trigger } = useSWRMutation(key, fetcher, { onError, onSuccess })
+
+ return trigger().catch(onRejected)}>trigger
+ }
+
+ render( )
+
+ await screen.findByText('trigger')
+ fireEvent.click(screen.getByText('trigger'))
+
+ await nextTick()
+
+ expect(onSuccess).not.toHaveBeenCalled()
+ expect(onError).toHaveBeenCalled()
+ expect(onRejected).toHaveBeenCalled()
+ })
})