Skip to content

Commit

Permalink
feat(react): add experimental DelaySuspense in v1.8.1
Browse files Browse the repository at this point in the history
  • Loading branch information
manudeli committed Feb 4, 2023
1 parent e56b648 commit 1f47032
Show file tree
Hide file tree
Showing 22 changed files with 210 additions and 60 deletions.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file modified .yarn/install-state.gz
Binary file not shown.
6 changes: 3 additions & 3 deletions packages/react-query/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@suspensive/react-query",
"version": "1.7.4",
"version": "1.8.1",
"keywords": [
"suspensive",
"react-query"
Expand Down Expand Up @@ -36,7 +36,7 @@
},
"devDependencies": {
"@suspensive/babel": "workspace:^",
"@suspensive/react": "workspace:^1.7.4",
"@suspensive/react": "workspace:^1.8.1",
"@suspensive/rollup": "workspace:^",
"@suspensive/tsconfig": "workspace:^",
"@tanstack/react-query": "^4.16.1",
Expand All @@ -50,7 +50,7 @@
"typescript": "^4.9.3"
},
"peerDependencies": {
"@suspensive/react": "^1.7.4",
"@suspensive/react": "^1.8.1",
"@tanstack/react-query": "^4.16.1",
"react": "^16.8 || ^17 || ^18"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@suspensive/react",
"version": "1.7.4",
"version": "1.8.1",
"keywords": [
"suspensive",
"react"
Expand Down
9 changes: 4 additions & 5 deletions packages/react/src/AsyncBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { ComponentProps, ComponentRef, ComponentType, forwardRef } from 'react'
import { ErrorBoundary } from './ErrorBoundary'
import { Suspense } from './Suspense'
import { ComponentPropsWithoutChildren } from './types'
import { isDevelopment } from './utils'

type SuspenseProps = ComponentProps<typeof Suspense>
type ErrorBoundaryProps = ComponentProps<typeof ErrorBoundary>
Expand All @@ -19,7 +18,7 @@ const BaseAsyncBoundary = forwardRef<ComponentRef<typeof ErrorBoundary>, AsyncBo
</ErrorBoundary>
)
)
if (isDevelopment) {
if (process.env.NODE_ENV !== 'production') {
BaseAsyncBoundary.displayName = 'AsyncBoundary'
}
const CSROnlyAsyncBoundary = forwardRef<ComponentRef<typeof ErrorBoundary>, AsyncBoundaryProps>(
Expand All @@ -29,7 +28,7 @@ const CSROnlyAsyncBoundary = forwardRef<ComponentRef<typeof ErrorBoundary>, Asyn
</ErrorBoundary>
)
)
if (isDevelopment) {
if (process.env.NODE_ENV !== 'production') {
CSROnlyAsyncBoundary.displayName = 'AsyncBoundary.CSROnly'
}

Expand All @@ -56,7 +55,7 @@ export const withAsyncBoundary = <Props extends Record<string, unknown> = Record
</AsyncBoundary>
)

if (isDevelopment) {
if (process.env.NODE_ENV !== 'production') {
const name = Component.displayName || Component.name || 'Component'
Wrapped.displayName = `withAsyncBoundary(${name})`
}
Expand All @@ -73,7 +72,7 @@ withAsyncBoundary.CSROnly = <Props extends Record<string, unknown> = Record<stri
</AsyncBoundary.CSROnly>
)

if (isDevelopment) {
if (process.env.NODE_ENV !== 'production') {
const name = Component.displayName || Component.name || 'Component'
Wrapped.displayName = `withAsyncBoundary.CSROnly(${name})`
}
Expand Down
79 changes: 79 additions & 0 deletions packages/react/src/DelaySuspense.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { ComponentProps, ComponentType, useEffect, useState } from 'react'
import { Suspense } from './Suspense'
import { ComponentPropsWithoutChildren } from './types'

type DelaySuspenseProps = ComponentProps<typeof Suspense> & {
ms?: number
}

const DefaultDelaySuspense = (props: DelaySuspenseProps) => (
<Suspense {...props} fallback={<Delay ms={props.ms}>{props.fallback}</Delay>} />
)
if (process.env.NODE_ENV !== 'production') {
DefaultDelaySuspense.displayName = 'DelaySuspense'
}
const CSROnlyDelaySuspense = (props: DelaySuspenseProps) => (
<Suspense.CSROnly {...props} fallback={<Delay ms={props.ms}>{props.fallback}</Delay>} />
)
if (process.env.NODE_ENV !== 'production') {
CSROnlyDelaySuspense.displayName = 'DelaySuspense.CSROnly'
}

const Delay = ({ ms = 0, children }: Pick<DelaySuspenseProps, 'ms'> & { children: DelaySuspenseProps['fallback'] }) => {
const [isDelayed, setIsDelayed] = useState(ms === 0)

useEffect(() => {
const timerId = setTimeout(() => !isDelayed && setIsDelayed(true), ms)
return () => clearTimeout(timerId)
}, [])

return <>{isDelayed ? children : null}</>
}

/**
* This component can accept delay time(ms) as prop to prevent to show fallback directly when children is suspended
* @experimental This component can be renamed itself or be merged to default Suspense of suspensive/react.
*/
export const DelaySuspense = DefaultDelaySuspense as typeof DefaultDelaySuspense & {
/**
* CSROnly mode make DelaySuspense can be used in SSR framework like Next.js with React 17 or under
* @experimental This component can be renamed itself or be merged to default Suspense of suspensive/react.
*/
CSROnly: typeof CSROnlyDelaySuspense
}
DelaySuspense.CSROnly = CSROnlyDelaySuspense

export function withDelaySuspense<Props extends Record<string, unknown> = Record<string, never>>(
Component: ComponentType<Props>,
suspenseProps?: ComponentPropsWithoutChildren<typeof DelaySuspense>
) {
const Wrapped = (props: Props) => (
<DelaySuspense {...suspenseProps}>
<Component {...props} />
</DelaySuspense>
)

if (process.env.NODE_ENV !== 'production') {
const name = Component.displayName || Component.name || 'Component'
Wrapped.displayName = `withDelaySuspense(${name})`
}

return Wrapped
}

withDelaySuspense.CSROnly = function withDelaySuspenseCSROnly<
Props extends Record<string, unknown> = Record<string, never>
>(Component: ComponentType<Props>, suspenseProps?: ComponentPropsWithoutChildren<typeof DelaySuspense.CSROnly>) {
const Wrapped = (props: Props) => (
<DelaySuspense.CSROnly {...suspenseProps}>
<Component {...props} />
</DelaySuspense.CSROnly>
)

if (process.env.NODE_ENV !== 'production') {
const name = Component.displayName || Component.name || 'Component'
Wrapped.displayName = `withDelaySuspense.CSROnly(${name})`
}

return Wrapped
}
6 changes: 3 additions & 3 deletions packages/react/src/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from 'react'
import { ErrorBoundaryGroupContext } from './ErrorBoundaryGroup'
import { ComponentPropsWithoutChildren } from './types'
import { isDevelopment, isDifferentArray } from './utils'
import { isDifferentArray } from './utils'

type ErrorBoundaryProps = PropsWithRef<
PropsWithChildren<{
Expand Down Expand Up @@ -123,7 +123,7 @@ export const ErrorBoundary = forwardRef<{ reset(): void }, ComponentPropsWithout
return <BaseErrorBoundary {...props} resetKeys={resetKeys} ref={ref} />
}
)
if (isDevelopment) {
if (process.env.NODE_ENV !== 'production') {
ErrorBoundary.displayName = 'ErrorBoundary'
}

Expand All @@ -137,7 +137,7 @@ export const withErrorBoundary = <Props extends Record<string, unknown> = Record
</ErrorBoundary>
)

if (isDevelopment) {
if (process.env.NODE_ENV !== 'production') {
const name = Component.displayName || Component.name || 'Component'
Wrapped.displayName = `withErrorBoundary(${name})`
}
Expand Down
5 changes: 2 additions & 3 deletions packages/react/src/ErrorBoundaryGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { ComponentType, ReactNode, createContext, useContext, useEffect, useMemo } from 'react'
import { useIsMounted, useKey } from './hooks'
import { ComponentPropsWithoutChildren } from './types'
import { isDevelopment } from './utils'

export const ErrorBoundaryGroupContext = createContext({ resetKey: 0, reset: () => {} })
if (isDevelopment) {
if (process.env.NODE_ENV !== 'production') {
ErrorBoundaryGroupContext.displayName = 'ErrorBoundaryGroupContext'
}

Expand Down Expand Up @@ -81,7 +80,7 @@ export const withErrorBoundaryGroup = <Props extends Record<string, unknown> = R
</ErrorBoundaryGroup>
)

if (isDevelopment) {
if (process.env.NODE_ENV !== 'production') {
const name = Component.displayName || Component.name || 'Component'
Wrapped.displayName = `withErrorBoundaryGroup(${name})`
}
Expand Down
9 changes: 4 additions & 5 deletions packages/react/src/Suspense.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { Suspense as BaseSuspense, ComponentType, SuspenseProps } from 'react'
import { useIsMounted } from './hooks'
import { ComponentPropsWithoutChildren } from './types'
import { isDevelopment } from './utils'

const DefaultSuspense = (props: SuspenseProps) => <BaseSuspense {...props} />
if (isDevelopment) {
if (process.env.NODE_ENV !== 'production') {
DefaultSuspense.displayName = 'Suspense'
}
const CSROnlySuspense = (props: SuspenseProps) => (useIsMounted() ? <BaseSuspense {...props} /> : <>{props.fallback}</>)
if (isDevelopment) {
if (process.env.NODE_ENV !== 'production') {
CSROnlySuspense.displayName = 'Suspense.CSROnly'
}

Expand All @@ -35,7 +34,7 @@ export function withSuspense<Props extends Record<string, unknown> = Record<stri
</Suspense>
)

if (isDevelopment) {
if (process.env.NODE_ENV !== 'production') {
const name = Component.displayName || Component.name || 'Component'
Wrapped.displayName = `withSuspense(${name})`
}
Expand All @@ -53,7 +52,7 @@ withSuspense.CSROnly = function withSuspenseCSROnly<Props extends Record<string,
</Suspense.CSROnly>
)

if (isDevelopment) {
if (process.env.NODE_ENV !== 'production') {
const name = Component.displayName || Component.name || 'Component'
Wrapped.displayName = `withSuspense.CSROnly(${name})`
}
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export { AsyncBoundary, withAsyncBoundary } from './AsyncBoundary'
export { Suspense, withSuspense } from './Suspense'
export { ErrorBoundary, withErrorBoundary } from './ErrorBoundary'
export { ErrorBoundaryGroup, useErrorBoundaryGroup, withErrorBoundaryGroup } from './ErrorBoundaryGroup'
export { DelaySuspense, withDelaySuspense } from './DelaySuspense'
1 change: 0 additions & 1 deletion packages/react/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export { default as isDifferentArray } from './isDifferentArray'
export { default as isDevelopment } from './isDevelopment'
3 changes: 0 additions & 3 deletions packages/react/src/utils/isDevelopment.ts

This file was deleted.

10 changes: 5 additions & 5 deletions websites/visualization/components/forPlayground/api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import axios from 'axios'

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

type Post = { id: number; title: string; body: string; userId: number }
type Album = { id: number; title: string; userId: number }
type Todo = { id: number; title: string; completed: boolean; userId: number }
export type Post = { id: number; title: string; body: string; userId: number }
export type Album = { id: number; title: string; userId: number }
export type Todo = { id: number; title: string; completed: boolean; userId: number }

export const posts = {
getMany: async () => {
await delay()
return axios.get<Post[]>('https://jsonplaceholder.typicode.com/posts').then(({ data }) => data.splice(0, 5))
await delay(Math.random() * 2000)
return axios.get<Post[]>('https://jsonplaceholder.typicode.com/posts').then(({ data }) => data)
},
getOneBy: async ({ id }: { id: Post['id'] }) => {
await delay(Math.random() * 3000)
Expand Down
36 changes: 28 additions & 8 deletions websites/visualization/components/forPlayground/suspensive.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,46 @@
import { Suspense } from '@suspensive/react'
import { useSuspenseQuery } from '@suspensive/react-query'
import { albums, posts, todos } from './api'
import { albums, Post, posts, todos } from './api'
import { Spinner } from '../uis'
import { useIntersectionObserver } from './useIntersectionObserver'
import { useEffect, useRef, useState } from 'react'

export const PostListSuspensive = () => {
const postsQuery = useSuspenseQuery(['posts'], posts.getMany)

return (
<ul style={{ maxWidth: 600 }}>
{postsQuery.data.map((post) => (
<li key={post.id}>
<h3>Title: {post.title}</h3>
<Suspense.CSROnly fallback={<Spinner />}>
<Post id={post.id} />
</Suspense.CSROnly>
</li>
<PostListItem key={post.id} post={post} />
))}
</ul>
)
}

export const Post = ({ id }: { id: number }) => {
const PostListItem = ({ post }: { post: Post }) => {
const ref = useRef(null)
const entry = useIntersectionObserver(ref)
const [isShow, setIsShow] = useState(false)

useEffect(() => {
if (entry?.isIntersecting) {
setIsShow(true)
}
}, [entry?.isIntersecting])

return (
<li key={post.id} ref={ref} style={{ minHeight: 200 }}>
<h3>Title: {post.title}</h3>
{isShow && (
<Suspense.CSROnly fallback={<Spinner />}>
<PostContent id={post.id} />
</Suspense.CSROnly>
)}
</li>
)
}

const PostContent = ({ id }: { id: number }) => {
const postQuery = useSuspenseQuery(['posts', id], () => posts.getOneBy({ id }))
const albumsQuery = useSuspenseQuery(['users', postQuery.data.userId, 'albums'], () =>
albums.getManyBy({ userId: postQuery.data.userId })
Expand Down
30 changes: 24 additions & 6 deletions websites/visualization/components/forPlayground/tanstack.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useQuery } from '@tanstack/react-query'
import { albums, posts, todos } from './api'
import { albums, Post, posts, todos } from './api'
import { Spinner } from '../uis'
import { useEffect, useRef, useState } from 'react'
import { useIntersectionObserver } from './useIntersectionObserver'

export const PostListTanStack = () => {
const postsQuery = useQuery(['posts'], posts.getMany)
Expand All @@ -15,16 +17,32 @@ export const PostListTanStack = () => {
return (
<ul style={{ maxWidth: 600 }}>
{postsQuery.data.map((post) => (
<li key={post.id}>
<h3>Title: {post.title}</h3>
<Post id={post.id} />
</li>
<PostListItem key={post.id} post={post} />
))}
</ul>
)
}

export const Post = ({ id }: { id: number }) => {
const PostListItem = ({ post }: { post: Post }) => {
const ref = useRef(null)
const entry = useIntersectionObserver(ref)
const [isShow, setIsShow] = useState(false)

useEffect(() => {
if (entry?.isIntersecting) {
setIsShow(true)
}
}, [entry?.isIntersecting])

return (
<li key={post.id} ref={ref} style={{ minHeight: 200 }}>
<h3>Title: {post.title}</h3>
{isShow && <PostContent id={post.id} />}
</li>
)
}

const PostContent = ({ id }: { id: number }) => {
const postQuery = useQuery(['posts', id], () => posts.getOneBy({ id }))
const albumsQuery = useQuery(
['users', postQuery.data?.userId, 'albums'],
Expand Down
Loading

0 comments on commit 1f47032

Please sign in to comment.