diff --git a/.yarn/cache/@suspensive-react-npm-1.7.3-1f5049709e-9f51378f46.zip b/.yarn/cache/@suspensive-react-npm-1.7.3-1f5049709e-9f51378f46.zip deleted file mode 100644 index 4045f2183..000000000 Binary files a/.yarn/cache/@suspensive-react-npm-1.7.3-1f5049709e-9f51378f46.zip and /dev/null differ diff --git a/.yarn/cache/@suspensive-react-npm-1.8.0-bf1f1ebb0b-6231aab277.zip b/.yarn/cache/@suspensive-react-npm-1.8.0-bf1f1ebb0b-6231aab277.zip new file mode 100644 index 000000000..9db0c5b4d Binary files /dev/null and b/.yarn/cache/@suspensive-react-npm-1.8.0-bf1f1ebb0b-6231aab277.zip differ diff --git a/.yarn/cache/@suspensive-react-query-npm-1.7.3-150c153164-4837bc5f86.zip b/.yarn/cache/@suspensive-react-query-npm-1.7.3-150c153164-4837bc5f86.zip deleted file mode 100644 index 78fcacc66..000000000 Binary files a/.yarn/cache/@suspensive-react-query-npm-1.7.3-150c153164-4837bc5f86.zip and /dev/null differ diff --git a/.yarn/cache/@suspensive-react-query-npm-1.8.0-a72d19915b-b1f350f4f4.zip b/.yarn/cache/@suspensive-react-query-npm-1.8.0-a72d19915b-b1f350f4f4.zip new file mode 100644 index 000000000..09a6e9093 Binary files /dev/null and b/.yarn/cache/@suspensive-react-query-npm-1.8.0-a72d19915b-b1f350f4f4.zip differ diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz index a13ae82d3..25e18eb86 100644 Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ diff --git a/packages/react-query/package.json b/packages/react-query/package.json index a6d1abdde..8275a5f2a 100644 --- a/packages/react-query/package.json +++ b/packages/react-query/package.json @@ -1,6 +1,6 @@ { "name": "@suspensive/react-query", - "version": "1.7.4", + "version": "1.8.1", "keywords": [ "suspensive", "react-query" @@ -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", @@ -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" }, diff --git a/packages/react/package.json b/packages/react/package.json index 588dcd122..423a988bf 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@suspensive/react", - "version": "1.7.4", + "version": "1.8.1", "keywords": [ "suspensive", "react" diff --git a/packages/react/src/AsyncBoundary.tsx b/packages/react/src/AsyncBoundary.tsx index f62d5d96d..3ebad868b 100644 --- a/packages/react/src/AsyncBoundary.tsx +++ b/packages/react/src/AsyncBoundary.tsx @@ -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 type ErrorBoundaryProps = ComponentProps @@ -19,7 +18,7 @@ const BaseAsyncBoundary = forwardRef, AsyncBo ) ) -if (isDevelopment) { +if (process.env.NODE_ENV !== 'production') { BaseAsyncBoundary.displayName = 'AsyncBoundary' } const CSROnlyAsyncBoundary = forwardRef, AsyncBoundaryProps>( @@ -29,7 +28,7 @@ const CSROnlyAsyncBoundary = forwardRef, Asyn ) ) -if (isDevelopment) { +if (process.env.NODE_ENV !== 'production') { CSROnlyAsyncBoundary.displayName = 'AsyncBoundary.CSROnly' } @@ -56,7 +55,7 @@ export const withAsyncBoundary = = Record ) - if (isDevelopment) { + if (process.env.NODE_ENV !== 'production') { const name = Component.displayName || Component.name || 'Component' Wrapped.displayName = `withAsyncBoundary(${name})` } @@ -73,7 +72,7 @@ withAsyncBoundary.CSROnly = = Record ) - if (isDevelopment) { + if (process.env.NODE_ENV !== 'production') { const name = Component.displayName || Component.name || 'Component' Wrapped.displayName = `withAsyncBoundary.CSROnly(${name})` } diff --git a/packages/react/src/DelaySuspense.tsx b/packages/react/src/DelaySuspense.tsx new file mode 100644 index 000000000..63bd40c41 --- /dev/null +++ b/packages/react/src/DelaySuspense.tsx @@ -0,0 +1,79 @@ +import { ComponentProps, ComponentType, useEffect, useState } from 'react' +import { Suspense } from './Suspense' +import { ComponentPropsWithoutChildren } from './types' + +type DelaySuspenseProps = ComponentProps & { + ms?: number +} + +const DefaultDelaySuspense = (props: DelaySuspenseProps) => ( + {props.fallback}} /> +) +if (process.env.NODE_ENV !== 'production') { + DefaultDelaySuspense.displayName = 'DelaySuspense' +} +const CSROnlyDelaySuspense = (props: DelaySuspenseProps) => ( + {props.fallback}} /> +) +if (process.env.NODE_ENV !== 'production') { + CSROnlyDelaySuspense.displayName = 'DelaySuspense.CSROnly' +} + +const Delay = ({ ms = 0, children }: Pick & { 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 = Record>( + Component: ComponentType, + suspenseProps?: ComponentPropsWithoutChildren +) { + const Wrapped = (props: Props) => ( + + + + ) + + 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 = Record +>(Component: ComponentType, suspenseProps?: ComponentPropsWithoutChildren) { + const Wrapped = (props: Props) => ( + + + + ) + + if (process.env.NODE_ENV !== 'production') { + const name = Component.displayName || Component.name || 'Component' + Wrapped.displayName = `withDelaySuspense.CSROnly(${name})` + } + + return Wrapped +} diff --git a/packages/react/src/ErrorBoundary.tsx b/packages/react/src/ErrorBoundary.tsx index a5905748e..2620db9d4 100644 --- a/packages/react/src/ErrorBoundary.tsx +++ b/packages/react/src/ErrorBoundary.tsx @@ -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<{ @@ -123,7 +123,7 @@ export const ErrorBoundary = forwardRef<{ reset(): void }, ComponentPropsWithout return } ) -if (isDevelopment) { +if (process.env.NODE_ENV !== 'production') { ErrorBoundary.displayName = 'ErrorBoundary' } @@ -137,7 +137,7 @@ export const withErrorBoundary = = Record ) - if (isDevelopment) { + if (process.env.NODE_ENV !== 'production') { const name = Component.displayName || Component.name || 'Component' Wrapped.displayName = `withErrorBoundary(${name})` } diff --git a/packages/react/src/ErrorBoundaryGroup.tsx b/packages/react/src/ErrorBoundaryGroup.tsx index 4487d60c0..2672042dc 100644 --- a/packages/react/src/ErrorBoundaryGroup.tsx +++ b/packages/react/src/ErrorBoundaryGroup.tsx @@ -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' } @@ -81,7 +80,7 @@ export const withErrorBoundaryGroup = = R ) - if (isDevelopment) { + if (process.env.NODE_ENV !== 'production') { const name = Component.displayName || Component.name || 'Component' Wrapped.displayName = `withErrorBoundaryGroup(${name})` } diff --git a/packages/react/src/Suspense.tsx b/packages/react/src/Suspense.tsx index ef88f651a..f4ba62bdb 100644 --- a/packages/react/src/Suspense.tsx +++ b/packages/react/src/Suspense.tsx @@ -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) => -if (isDevelopment) { +if (process.env.NODE_ENV !== 'production') { DefaultSuspense.displayName = 'Suspense' } const CSROnlySuspense = (props: SuspenseProps) => (useIsMounted() ? : <>{props.fallback}) -if (isDevelopment) { +if (process.env.NODE_ENV !== 'production') { CSROnlySuspense.displayName = 'Suspense.CSROnly' } @@ -35,7 +34,7 @@ export function withSuspense = Record ) - if (isDevelopment) { + if (process.env.NODE_ENV !== 'production') { const name = Component.displayName || Component.name || 'Component' Wrapped.displayName = `withSuspense(${name})` } @@ -53,7 +52,7 @@ withSuspense.CSROnly = function withSuspenseCSROnly ) - if (isDevelopment) { + if (process.env.NODE_ENV !== 'production') { const name = Component.displayName || Component.name || 'Component' Wrapped.displayName = `withSuspense.CSROnly(${name})` } diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index da61069a6..25666de12 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -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' diff --git a/packages/react/src/utils/index.ts b/packages/react/src/utils/index.ts index 9be00c8a2..5e6c42522 100644 --- a/packages/react/src/utils/index.ts +++ b/packages/react/src/utils/index.ts @@ -1,2 +1 @@ export { default as isDifferentArray } from './isDifferentArray' -export { default as isDevelopment } from './isDevelopment' diff --git a/packages/react/src/utils/isDevelopment.ts b/packages/react/src/utils/isDevelopment.ts deleted file mode 100644 index 19de2de1f..000000000 --- a/packages/react/src/utils/isDevelopment.ts +++ /dev/null @@ -1,3 +0,0 @@ -const isDevelopment = process.env.NODE_ENV !== 'production' - -export default isDevelopment diff --git a/websites/visualization/components/forPlayground/api.tsx b/websites/visualization/components/forPlayground/api.tsx index 5628fa34a..db262660e 100644 --- a/websites/visualization/components/forPlayground/api.tsx +++ b/websites/visualization/components/forPlayground/api.tsx @@ -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('https://jsonplaceholder.typicode.com/posts').then(({ data }) => data.splice(0, 5)) + await delay(Math.random() * 2000) + return axios.get('https://jsonplaceholder.typicode.com/posts').then(({ data }) => data) }, getOneBy: async ({ id }: { id: Post['id'] }) => { await delay(Math.random() * 3000) diff --git a/websites/visualization/components/forPlayground/suspensive.tsx b/websites/visualization/components/forPlayground/suspensive.tsx index 95fc6d9af..41c23d941 100644 --- a/websites/visualization/components/forPlayground/suspensive.tsx +++ b/websites/visualization/components/forPlayground/suspensive.tsx @@ -1,7 +1,9 @@ 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) @@ -9,18 +11,36 @@ export const PostListSuspensive = () => { return (
    {postsQuery.data.map((post) => ( -
  • -

    Title: {post.title}

    - }> - - -
  • + ))}
) } -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 ( +
  • +

    Title: {post.title}

    + {isShow && ( + }> + + + )} +
  • + ) +} + +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 }) diff --git a/websites/visualization/components/forPlayground/tanstack.tsx b/websites/visualization/components/forPlayground/tanstack.tsx index 563168935..bbb335639 100644 --- a/websites/visualization/components/forPlayground/tanstack.tsx +++ b/websites/visualization/components/forPlayground/tanstack.tsx @@ -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) @@ -15,16 +17,32 @@ export const PostListTanStack = () => { return (
      {postsQuery.data.map((post) => ( -
    • -

      Title: {post.title}

      - -
    • + ))}
    ) } -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 ( +
  • +

    Title: {post.title}

    + {isShow && } +
  • + ) +} + +const PostContent = ({ id }: { id: number }) => { const postQuery = useQuery(['posts', id], () => posts.getOneBy({ id })) const albumsQuery = useQuery( ['users', postQuery.data?.userId, 'albums'], diff --git a/websites/visualization/components/forPlayground/useIntersectionObserver.ts b/websites/visualization/components/forPlayground/useIntersectionObserver.ts new file mode 100644 index 000000000..cd5625716 --- /dev/null +++ b/websites/visualization/components/forPlayground/useIntersectionObserver.ts @@ -0,0 +1,39 @@ +import type { RefObject } from 'react' +import { useEffect, useState } from 'react' + +interface Args extends IntersectionObserverInit { + freezeOnceVisible?: boolean +} + +export function useIntersectionObserver( + elementRef: RefObject, + { threshold = 0, root = null, rootMargin = '0%', freezeOnceVisible = false }: Args = {} +): IntersectionObserverEntry | undefined { + const [entry, setEntry] = useState() + + const frozen = entry?.isIntersecting && freezeOnceVisible + + const updateEntry = ([entry]: IntersectionObserverEntry[]): void => { + setEntry(entry) + } + + useEffect(() => { + const node = elementRef?.current // DOM Ref + const hasIOSupport = !!window.IntersectionObserver + + if (!hasIOSupport || frozen || !node) { + return + } + + const observerParams = { threshold, root, rootMargin } + const observer = new IntersectionObserver(updateEntry, observerParams) + + observer.observe(node) + + return () => observer.disconnect() + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [elementRef, JSON.stringify(threshold), root, rootMargin, frozen]) + + return entry +} diff --git a/websites/visualization/components/uis/index.tsx b/websites/visualization/components/uis/index.tsx index cac5412fe..d686ba44e 100644 --- a/websites/visualization/components/uis/index.tsx +++ b/websites/visualization/components/uis/index.tsx @@ -1,4 +1,4 @@ -import { PropsWithChildren, ReactNode } from 'react' +import { PropsWithChildren } from 'react' import styled from '@emotion/styled' export const Button = styled.button` diff --git a/websites/visualization/package.json b/websites/visualization/package.json index 168072d0d..1c55d4fc2 100644 --- a/websites/visualization/package.json +++ b/websites/visualization/package.json @@ -9,8 +9,8 @@ "dependencies": { "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", - "@suspensive/react": "1.7.3", - "@suspensive/react-query": "1.7.3", + "@suspensive/react": "1.8.0", + "@suspensive/react-query": "1.8.0", "@tanstack/react-query": "^4.16.1", "@tanstack/react-query-devtools": "^4.16.1", "axios": "^1.2.0", diff --git a/yarn.lock b/yarn.lock index b8890fee4..3d0033eab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3433,14 +3433,14 @@ __metadata: languageName: unknown linkType: soft -"@suspensive/react-query@npm:1.7.3": - version: 1.7.3 - resolution: "@suspensive/react-query@npm:1.7.3" +"@suspensive/react-query@npm:1.8.0": + version: 1.8.0 + resolution: "@suspensive/react-query@npm:1.8.0" peerDependencies: - "@suspensive/react": ^1.7.3 + "@suspensive/react": ^1.8.0 "@tanstack/react-query": ^4.16.1 react: ^16.8 || ^17 || ^18 - checksum: 4837bc5f866ad928f9a0fd5da3f66be0049fa5e00c9d5e935a7745268db769d2b21834f16c5585224d8a7c1d5853142e8ddd4b1f831121b286f7691ee77d1464 + checksum: b1f350f4f444c2947f56036d3a1035d0fae53309e00bdae8abe433d337e0d031e513d5812c977457f2e4433674d9bc55cc0dd13f0b215a19d779b1855bc7093a languageName: node linkType: hard @@ -3449,7 +3449,7 @@ __metadata: resolution: "@suspensive/react-query@workspace:packages/react-query" dependencies: "@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 @@ -3462,22 +3462,22 @@ __metadata: rollup: ^3.5.1 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 languageName: unknown linkType: soft -"@suspensive/react@npm:1.7.3": - version: 1.7.3 - resolution: "@suspensive/react@npm:1.7.3" +"@suspensive/react@npm:1.8.0": + version: 1.8.0 + resolution: "@suspensive/react@npm:1.8.0" peerDependencies: react: ^16.8 || ^17 || ^18 - checksum: 9f51378f46de90e7bd6a306a6e61455df234e50c40a4a4e58047699885028cba1ec00314741f11cef5e49caf3a1ca307a8d72c45c3ffcfd725d0c4eb4473583c + checksum: 6231aab2771c5d8bf1cf038e90de147469ca66b2861e03d76a575f5531ddcc33cd2331ef40c1fbe0d6b9df82c69ac6b16cb026ace62e0facb1a553da4dbf5de6 languageName: node linkType: hard -"@suspensive/react@workspace:^1.7.4, @suspensive/react@workspace:packages/react": +"@suspensive/react@workspace:^1.8.1, @suspensive/react@workspace:packages/react": version: 0.0.0-use.local resolution: "@suspensive/react@workspace:packages/react" dependencies: @@ -3530,8 +3530,8 @@ __metadata: "@babel/core": ^7.0.0 "@emotion/react": ^11.10.5 "@emotion/styled": ^11.10.5 - "@suspensive/react": 1.7.3 - "@suspensive/react-query": 1.7.3 + "@suspensive/react": 1.8.0 + "@suspensive/react-query": 1.8.0 "@tanstack/react-query": ^4.16.1 "@tanstack/react-query-devtools": ^4.16.1 "@types/node": ^17.0.12