Skip to content

Commit

Permalink
feat(react-query-v4): react-query-next-experimental (#1161)
Browse files Browse the repository at this point in the history
# Overview

#1155 

Adds a streaming without prefetching feature that brings the
[react-query-next-experimental](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#experimental-streaming-without-prefetching-in-nextjs)
feature available in react-query v5 to v4

- [x] Add a default implementation
  - HydrationStreamProvider
  - ReactQueryStreamedHydration
- [x] ~~Test working well~~ continue with the follow-up.


<!--
    A clear and concise description of what this pr is about.
 -->

## PR Checklist

- [x] I did below actions if need

1. I read the [Contributing
Guide](https://github.com/toss/suspensive/blob/main/CONTRIBUTING.md)
2. I added documents and tests.

---------

Co-authored-by: Jonghyeon Ko <[email protected]>
Co-authored-by: Jonghyeon Ko <[email protected]>
  • Loading branch information
3 people authored Aug 18, 2024
1 parent c179a56 commit 29205c7
Show file tree
Hide file tree
Showing 47 changed files with 1,117 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/angry-turtles-thank.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@suspensive/react-query-next-experimental-4": patch
---

feat(react-query-next-experimental): react-query-next-experimental
2 changes: 2 additions & 0 deletions .github/ISSUE_TEMPLATE/bug.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ body:
- "@suspensive/react-query"
- "@suspensive/react-query-4"
- "@suspensive/react-query-5"
- "@suspensive/react-query-next-experimental"
- "@suspensive/react-query-next-experimental-4"
- "@suspensive/jotai"
- "@suspensive/cache"
- "@suspensive/react-image"
Expand Down
2 changes: 2 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ body:
- "@suspensive/react-query"
- "@suspensive/react-query-4"
- "@suspensive/react-query-5"
- "@suspensive/react-query-next-experimental"
- "@suspensive/react-query-next-experimental-4"
- "@suspensive/jotai"
- "@suspensive/cache"
- "@suspensive/react-image"
Expand Down
4 changes: 4 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
- "packages/react-query-4/**/*"
"@suspensive/react-query-5":
- "packages/react-query-5/**/*"
"@suspensive/react-query-next-experimental":
- "packages/react-query-next-experimental/**/*"
"@suspensive/react-query-next-experimental-4":
- "packages/react-query-next-experimental-4/**/*"
"@suspensive/jotai":
- "packages/jotai/**/*"
"@suspensive/cache":
Expand Down
8 changes: 8 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ component_management:
name: '@suspensive/react-query-5'
paths:
- packages/react-query-5/**
- component_id: react-query-next-experimental
name: '@suspensive/react-query-next-experimental'
paths:
- packages/react-query-next-experimental/**
- component_id: react-query-next-experimental-4
name: '@suspensive/react-query-next-experimental-4'
paths:
- packages/react-query-next-experimental-4/**
- component_id: jotai
name: '@suspensive/jotai'
paths:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
root: true,
extends: ['@suspensive/eslint-config/react-ts', 'plugin:@next/next/recommended'],
parserOptions: {
tsconfigRootDir: __dirname,
project: 'tsconfig.json',
},
}
35 changes: 35 additions & 0 deletions examples/react-query-next-experimental-4-example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
typedRoutes: true,
},
}

module.exports = nextConfig
31 changes: 31 additions & 0 deletions examples/react-query-next-experimental-4-example/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "@suspensive/react-query-next-experimental-4-example",
"version": "0.0.0",
"private": true,
"scripts": {
"build": "next build",
"ci:eslint": "next lint",
"ci:type": "tsc --noEmit",
"dev": "next dev -p 4100",
"start": "next start -p 4100"
},
"dependencies": {
"@suspensive/react": "workspace:*",
"@suspensive/react-query-4": "workspace:*",
"@suspensive/react-query-next-experimental-4": "workspace:*",
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-query-devtools": "^4.36.1",
"next": "^14.2.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwindcss": "^3.4.4"
},
"devDependencies": {
"@next/eslint-plugin-next": "^14.2.3",
"@suspensive/eslint-config": "workspace:*",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { NextResponse } from 'next/server'

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

export async function GET(request: Request) {
const ms = Number(new URL(request.url).searchParams.get('wait'))
await sleep(ms)
return NextResponse.json(`${new Date().toISOString()} success to get text waited after ${ms}ms`)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use client'

import { useSuspenseQuery } from '@suspensive/react-query-4'
import { type ComponentProps, forwardRef } from 'react'
import { query } from '~/query'

export const Text = forwardRef<HTMLParagraphElement, ComponentProps<'p'> & { ms: number }>(({ ms, ...props }, ref) => {
const { data: text } = useSuspenseQuery(query.text(ms))
return (
<p {...props} ref={ref}>
result: {text}
</p>
)
})
Text.displayName = 'Text'
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { type ComponentProps, forwardRef } from 'react'

export const Text2 = forwardRef<HTMLParagraphElement, ComponentProps<'p'>>((props, ref) => (
<p {...props} ref={ref}>
result: {props.children}
</p>
))
Text2.displayName = 'Text2'
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}

@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}

body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Metadata } from 'next'
import type { ReactNode } from 'react'
import { Providers } from './providers'

export const metadata: Metadata = {
title: 'Next HTML Streaming with Suspense',
}

export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
89 changes: 89 additions & 0 deletions examples/react-query-next-experimental-4-example/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
'use client'

import { Suspense } from '@suspensive/react'
import { SuspenseQuery } from '@suspensive/react-query-4'
import { useQueryClient } from '@tanstack/react-query'
import Link from 'next/link'
import { Text } from '~/app/components/Text'
import { Text2 } from '~/app/components/Text2'
import { query } from '~/query'

export default function Page() {
const queryClient = useQueryClient()

return (
<>
<Link href="/test">to test page</Link>
<Suspense>
<Text ms={100} />
</Suspense>
<Suspense>
<Text ms={200} />
</Suspense>
<Suspense>
<Text ms={300} />
</Suspense>
<Suspense>
<Text ms={400} />
</Suspense>
<Suspense>
<Text ms={500} />
</Suspense>
<Suspense>
<Text ms={600} />
</Suspense>
<Suspense>
<Text ms={700} />
</Suspense>

<button
onClick={() => {
queryClient.resetQueries()
}}
>
resetQueries all
</button>

<button
onClick={() => {
queryClient.invalidateQueries(query.text(500))
}}
>
invalidate 500
</button>

<button
onClick={() => {
queryClient.invalidateQueries(query.text(200))
}}
>
invalidate 200
</button>

<fieldset>
<legend>
combined <code>Suspense</code>-container
</legend>
<Suspense>
<Text ms={800} />
<Text ms={900} />
<Text ms={1000} />
</Suspense>
</fieldset>

<pre>{`Proposal: <SuspenseQuery /> Component`}</pre>
{/* Need to proposal */}
<ul>
<Suspense>
<SuspenseQuery {...query.text(1100)}>{({ data }) => <Text2>{data}</Text2>}</SuspenseQuery>
</Suspense>
<Suspense>
<SuspenseQuery {...query.text(1200)}>{({ data }) => <Text2>{data}</Text2>}</SuspenseQuery>
</Suspense>
<Suspense>
<SuspenseQuery {...query.text(1300)}>{({ data }) => <Text2>{data}</Text2>}</SuspenseQuery>
</Suspense>
</ul>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use client'

import { Suspensive, SuspensiveProvider } from '@suspensive/react'
import { ReactQueryStreamedHydration } from '@suspensive/react-query-next-experimental-4'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { type ReactNode, useState } from 'react'

export function Providers(props: { children: ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 1000,
},
},
})
)
const [suspensive] = useState(
() =>
new Suspensive({
defaultProps: {
suspense: { fallback: <div>loading...</div> },
},
})
)

return (
<SuspensiveProvider value={suspensive}>
<QueryClientProvider client={queryClient}>
<ReactQueryStreamedHydration>{props.children}</ReactQueryStreamedHydration>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</SuspensiveProvider>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Link from 'next/link'

export default function page() {
return (
<div>
page
<Link href="/">to home page</Link>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { queryOptions } from '@suspensive/react-query-4'

const baseURL = (() => {
if (typeof window !== 'undefined') return ''
if (process.env.NEXT_PUBLIC_STREAMING_HTML_URL) return `https://${process.env.NEXT_PUBLIC_STREAMING_HTML_URL}`
return 'http://localhost:4100'
})()

export const query = {
text: <TMs extends number>(ms: TMs) =>
queryOptions({
queryKey: ['query.text', ms],
queryFn: () =>
fetch(`${baseURL}/api/text?wait=${ms}`, {
cache: 'no-store',
}).then(
(res) =>
res.json() as unknown as `${ReturnType<Date['toISOString']>} success to get text waited after ${TMs}ms`
),
}),
}
Loading

0 comments on commit 29205c7

Please sign in to comment.