Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions e2e/site/app/concurrent-transition/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use client'

import { Suspense } from 'react'
import dynamic from 'next/dynamic'

const TransitionDemo = dynamic(() => import('./transition-demo'), {
ssr: false
})

export default function ConcurrentTransitionPage() {
return (
<div style={{ padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
<h1>React 19 Concurrent Transition Test</h1>
<p>
This page tests SWR&apos;s behavior with React 19 concurrent
transitions. When using useTransition, SWR should &quot;pause&quot;
loading states to provide smooth UX.
</p>
<Suspense fallback={<div>Loading page...</div>}>
<TransitionDemo />
</Suspense>
</div>
)
}
61 changes: 61 additions & 0 deletions e2e/site/app/concurrent-transition/transition-demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use client'

import React, { useState, useTransition, Suspense, useCallback } from 'react'
import useSWR from 'swr'

// Simulate API data fetching with delay
const fetcher = async (key: string): Promise<string> => {
// Slightly longer delay to make transition behavior more observable
await new Promise(resolve => setTimeout(resolve, 150))
return key
}

// Component that uses SWR with suspense
function DataComponent({ swrKey }: { swrKey: string }) {
const { data } = useSWR(swrKey, fetcher, {
dedupingInterval: 0,
suspense: true,
// React 19 improvements for concurrent features
keepPreviousData: false
})

return <span data-testid="data-content">data:{data}</span>
}

export default function TransitionDemo() {
const [isPending, startTransition] = useTransition()
const [key, setKey] = useState('initial-key')

const handleTransition = useCallback(() => {
startTransition(() => {
setKey('new-key')
})
}, [])

return (
<div>
<h2>React 19 Concurrent Transition Demo</h2>
<div
onClick={handleTransition}
data-testid="transition-trigger"
style={{
cursor: 'pointer',
padding: '20px',
border: '1px solid #ccc',
borderRadius: '4px',
backgroundColor: isPending ? '#f0f0f0' : '#fff'
}}
>
<div data-testid="pending-state">isPending:{isPending ? '1' : '0'}</div>
<Suspense
fallback={<span data-testid="loading-fallback">loading</span>}
>
<DataComponent swrKey={key} />
</Suspense>
<p style={{ fontSize: '12px', color: '#666', marginTop: '10px' }}>
Click to test concurrent transition behavior
</p>
</div>
</div>
)
}
2 changes: 1 addition & 1 deletion e2e/site/next-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
/// <reference types="next/navigation-types/compat/navigation" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
8 changes: 4 additions & 4 deletions e2e/site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
"@types/node": "^20.2.5",
"@types/react": "^18.2.8",
"@types/react-dom": "18.2.4",
"next": "^15.0.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"next": "^15.4.4",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"typescript": "5.1.3",
"swr": "*"
"swr": "link:../../"
}
}
38 changes: 38 additions & 0 deletions e2e/test/concurrent-transition.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/* eslint-disable testing-library/prefer-screen-queries */
import { test, expect } from '@playwright/test'

test.describe('concurrent rendering transitions', () => {
test('should pause when changing the key inside a transition', async ({
page
}) => {
// Navigate to the test page
await page.goto('./concurrent-transition', { waitUntil: 'networkidle' })

// Wait for page to be fully loaded and interactive
await expect(page.getByTestId('pending-state')).toContainText('isPending:0')

// Wait for initial data to load
await expect(page.getByTestId('data-content')).toContainText(
'data:initial-key'
)

// Ensure the component is in a stable state before triggering transition
await page.waitForTimeout(100)

// Click to trigger transition
await page.getByTestId('transition-trigger').click()

// Verify transition starts - isPending becomes true
await expect(page.getByTestId('pending-state')).toContainText('isPending:1')

// During transition: data should still show old value (this is the key behavior)
// In React 19, this behavior should be more consistent
await expect(page.getByTestId('data-content')).toContainText(
'data:initial-key'
)

// Wait for transition to complete
await expect(page.getByTestId('pending-state')).toContainText('isPending:0')
await expect(page.getByTestId('data-content')).toContainText('data:new-key')
})
})
12 changes: 8 additions & 4 deletions e2e/test/initial-render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,32 @@ test.describe('rendering', () => {
await page.getByRole('button', { name: 'preload' }).click()
await expect(page.getByText('suspense-after-preload')).toBeVisible()
})
test('should be able to retry in suspense with react 18.3', async ({

test('should be able to retry in suspense with react 19 (app router)', async ({
page
}) => {
await page.goto('./suspense-retry-18-3', { waitUntil: 'commit' })
await page.goto('./suspense-retry', { waitUntil: 'commit' })
await expect(page.getByText('Something went wrong')).toBeVisible()
await page.getByRole('button', { name: 'retry' }).click()
await expect(page.getByText('data: SWR suspense retry works')).toBeVisible()
})
test('should be able to retry in suspense with react 18.2', async ({

test('should be able to retry in suspense with react 19 (pages router)', async ({
page
}) => {
await page.goto('./suspense-retry-18-2', { waitUntil: 'commit' })
await page.goto('./suspense-retry-19', { waitUntil: 'commit' })
await expect(page.getByText('Something went wrong')).toBeVisible()
await page.getByRole('button', { name: 'retry' }).click()
await expect(page.getByText('data: SWR suspense retry works')).toBeVisible()
})

test('should be able to retry in suspense with mutate', async ({ page }) => {
await page.goto('./suspense-retry-mutate', { waitUntil: 'commit' })
await expect(page.getByText('Something went wrong')).toBeVisible()
await page.getByRole('button', { name: 'retry' }).click()
await expect(page.getByText('data: SWR suspense retry works')).toBeVisible()
})

test('should be able to use `unstable_serialize` in server component', async ({
page
}) => {
Expand Down
2 changes: 1 addition & 1 deletion examples/suspense-retry/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"@types/node": "^20.2.5",
"@types/react": "^18.2.8",
"@types/react-dom": "18.2.4",
"next": "^13.4.4",
"next": "^latest",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "5.1.3",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"lint-staged": "13.2.2",
"next": "15.0.4",
"next": "15.4.4",
"prettier": "2.8.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
Loading
Loading