diff --git a/.gitignore b/.gitignore index ddddc018b..28d246ec9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,6 @@ # dependencies node_modules -.pnp -.pnp.js # testing coverage @@ -19,8 +17,6 @@ build # debug npm-debug.log* -yarn-debug.log* -yarn-error.log* .pnpm-debug.log* # local env files diff --git a/packages/react-query/package.json b/packages/react-query/package.json index 9929a94b2..ea4ecfc57 100644 --- a/packages/react-query/package.json +++ b/packages/react-query/package.json @@ -1,6 +1,6 @@ { "name": "@suspensive/react-query", - "version": "1.8.1", + "version": "1.9.2", "keywords": [ "suspensive", "react-query" @@ -29,7 +29,7 @@ "scripts": { "build": "rm -rf dist esm && tsc -p tsconfig.json --declaration --emitDeclarationOnly --declarationDir dist && rollup -c rollup.config.js", "lint": "eslint .", - "npm:publish": "pnpm npm publish", + "npm:publish": "pnpm publish", "prepack": "pnpm build", "test": "echo \"Error: no test specified\" && exit 1", "type:check": "tsc --noEmit" @@ -37,7 +37,7 @@ "devDependencies": { "@suspensive/babel": "*", "@suspensive/eslint": "*", - "@suspensive/react": "^1.8.1", + "@suspensive/react": "1.9.2", "@suspensive/rollup": "*", "@suspensive/tsconfig": "*", "@tanstack/react-query": "^4.16.1", @@ -51,7 +51,7 @@ "typescript": "^4.9.3" }, "peerDependencies": { - "@suspensive/react": "^1.8.1", + "@suspensive/react": "1.9.2", "@tanstack/react-query": "^4.16.1", "react": "^16.8 || ^17 || ^18" }, @@ -65,9 +65,9 @@ }, "./package.json": "./package.json" }, - "import": "./esm/index.mjs", - "main": "./dist/index.js", - "module": "./esm/index.mjs", - "types": "./dist/index.d.ts" + "import": "esm/index.mjs", + "main": "dist/index.js", + "module": "esm/index.mjs", + "types": "dist/index.d.ts" } } diff --git a/packages/react/package.json b/packages/react/package.json index 58be9058e..c7b852538 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@suspensive/react", - "version": "1.8.1", + "version": "1.9.2", "keywords": [ "suspensive", "react" @@ -29,7 +29,7 @@ "scripts": { "build": "rm -rf dist esm && tsc -p tsconfig.json --declaration --emitDeclarationOnly --declarationDir dist && rollup -c rollup.config.js", "lint": "eslint .", - "npm:publish": "pnpm npm publish", + "npm:publish": "pnpm publish", "prepack": "pnpm build", "test": "echo \"Error: no test specified\" && exit 1", "type:check": "tsc --noEmit" @@ -61,9 +61,9 @@ }, "./package.json": "./package.json" }, - "import": "./esm/index.mjs", - "main": "./dist/index.js", - "module": "./esm/index.mjs", - "types": "./dist/index.d.ts" + "import": "esm/index.mjs", + "main": "dist/index.js", + "module": "esm/index.mjs", + "types": "dist/index.d.ts" } } diff --git a/packages/react/src/Delay.tsx b/packages/react/src/Delay.tsx new file mode 100644 index 000000000..8f17cacf0 --- /dev/null +++ b/packages/react/src/Delay.tsx @@ -0,0 +1,41 @@ +import { ComponentType, ReactNode, createContext, useContext, useEffect, useState } from 'react' +import { ComponentPropsWithoutChildren } from './types' + +export const DelayContext = createContext<{ ms?: number }>({ ms: 0 }) + +type DelayProps = { ms?: number; children: ReactNode } +/** + * @experimental This is experimental feature. + */ +export const Delay = ({ ms, children }: DelayProps) => { + const delayMs = ms ?? useContext(DelayContext).ms ?? 0 + const [isDelayed, setIsDelayed] = useState(delayMs === 0) + + useEffect(() => { + const timerId = setTimeout(() => !isDelayed && setIsDelayed(true), delayMs) + return () => clearTimeout(timerId) + }, [delayMs]) + + return <>{isDelayed ? children : null} +} + +/** + * @experimental This is experimental feature. + */ +export const withDelay = = Record>( + Component: ComponentType, + delayProps?: ComponentPropsWithoutChildren +) => { + const Wrapped = (props: Props) => ( + + + + ) + + if (process.env.NODE_ENV !== 'production') { + const name = Component.displayName || Component.name || 'Component' + Wrapped.displayName = `withDelay(${name})` + } + + return Wrapped +} diff --git a/packages/react/src/DelaySuspense.tsx b/packages/react/src/DelaySuspense.tsx deleted file mode 100644 index 63bd40c41..000000000 --- a/packages/react/src/DelaySuspense.tsx +++ /dev/null @@ -1,79 +0,0 @@ -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/Suspense.tsx b/packages/react/src/Suspense.tsx index f4ba62bdb..93ab67304 100644 --- a/packages/react/src/Suspense.tsx +++ b/packages/react/src/Suspense.tsx @@ -1,12 +1,28 @@ -import { Suspense as BaseSuspense, ComponentType, SuspenseProps } from 'react' +import { ComponentType, ReactNode, Suspense as ReactSuspense, SuspenseProps, createContext, useContext } from 'react' import { useIsMounted } from './hooks' import { ComponentPropsWithoutChildren } from './types' -const DefaultSuspense = (props: SuspenseProps) => +export const SuspenseContext = createContext<{ fallback?: ReactNode }>({ fallback: undefined }) +const useFallbackWithContext = (fallback: ReactNode) => { + const contextFallback = useContext(SuspenseContext).fallback + + return fallback === null ? null : fallback ?? contextFallback +} + +const DefaultSuspense = (props: SuspenseProps) => { + const fallback = useFallbackWithContext(props.fallback) + + return +} if (process.env.NODE_ENV !== 'production') { DefaultSuspense.displayName = 'Suspense' } -const CSROnlySuspense = (props: SuspenseProps) => (useIsMounted() ? : <>{props.fallback}) +const CSROnlySuspense = (props: SuspenseProps) => { + const isMounted = useIsMounted() + const fallback = useFallbackWithContext(props.fallback) + + return isMounted ? : <>{fallback} +} if (process.env.NODE_ENV !== 'production') { CSROnlySuspense.displayName = 'Suspense.CSROnly' } diff --git a/packages/react/src/SuspensiveProvider.tsx b/packages/react/src/SuspensiveProvider.tsx new file mode 100644 index 000000000..f99cdf59e --- /dev/null +++ b/packages/react/src/SuspensiveProvider.tsx @@ -0,0 +1,35 @@ +import { ContextType, ReactNode, useMemo } from 'react' +import { DelayContext } from './Delay' +import { SuspenseContext } from './Suspense' + +type Configs = { + defaultOptions?: { + suspense?: ContextType + delay?: ContextType + } +} + +/** + * @experimental This is experimental feature. + */ +export class SuspensiveConfigs { + public defaultOptions?: Configs['defaultOptions'] + + constructor(config: Configs = {}) { + this.defaultOptions = config.defaultOptions + } +} + +/** + * @experimental This is experimental feature. + */ +export const SuspensiveProvider = ({ configs, children }: { configs: SuspensiveConfigs; children: ReactNode }) => { + const delayValue = useMemo(() => configs.defaultOptions?.delay || {}, []) + const suspenseValue = useMemo(() => configs.defaultOptions?.suspense || {}, []) + + return ( + + {children} + + ) +} diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 25666de12..955c5fd27 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -2,4 +2,5 @@ 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' +export { SuspensiveProvider, SuspensiveConfigs } from './SuspensiveProvider' +export { Delay, withDelay } from './Delay' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 75d4d581a..a1f1fbf0e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -129,7 +129,7 @@ importers: specifiers: '@suspensive/babel': '*' '@suspensive/eslint': '*' - '@suspensive/react': ^1.8.1 + '@suspensive/react': 1.9.2 '@suspensive/rollup': '*' '@suspensive/tsconfig': '*' '@tanstack/react-query': ^4.16.1 @@ -201,8 +201,8 @@ importers: '@babel/core': ^7.0.0 '@emotion/react': ^11.10.5 '@emotion/styled': ^11.10.5 - '@suspensive/react': 1.8.1 - '@suspensive/react-query': 1.8.1 + '@suspensive/react': '*' + '@suspensive/react-query': '*' '@tanstack/react-query': ^4.16.1 '@tanstack/react-query-devtools': ^4.16.1 '@types/node': ^17.0.12 diff --git a/websites/visualization/components/forPlayground/api.tsx b/websites/visualization/components/forPlayground/api.tsx index db262660e..d2becfbbe 100644 --- a/websites/visualization/components/forPlayground/api.tsx +++ b/websites/visualization/components/forPlayground/api.tsx @@ -1,6 +1,7 @@ import axios from 'axios' const delay = (ms: number = 1000) => new Promise((resolve) => setTimeout(resolve, ms)) +const delayRandom = (maxMs: number = 1000) => delay(maxMs * Math.random()) export type Post = { id: number; title: string; body: string; userId: number } export type Album = { id: number; title: string; userId: number } @@ -8,18 +9,18 @@ export type Todo = { id: number; title: string; completed: boolean; userId: numb export const posts = { getMany: async () => { - await delay(Math.random() * 2000) + await delayRandom(3000) return axios.get('https://jsonplaceholder.typicode.com/posts').then(({ data }) => data) }, getOneBy: async ({ id }: { id: Post['id'] }) => { - await delay(Math.random() * 3000) + await delayRandom(3000) return axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`).then(({ data }) => data) }, } export const albums = { getManyBy: async ({ userId }: { userId: number }) => { - await delay(Math.random() * 3000) + await delayRandom(3000) return axios .get(`https://jsonplaceholder.typicode.com/users/${userId}/albums`) .then(({ data }) => data.splice(0, 2)) @@ -28,7 +29,7 @@ export const albums = { export const todos = { getManyBy: async ({ userId }: { userId: number }) => { - await delay(Math.random() * 3000) + await delayRandom(3000) return axios .get(`https://jsonplaceholder.typicode.com/users/${userId}/todos`) .then(({ data }) => data.splice(0, 2)) diff --git a/websites/visualization/components/forPlayground/suspensive.tsx b/websites/visualization/components/forPlayground/suspensive.tsx index 41c23d941..6ab20a926 100644 --- a/websites/visualization/components/forPlayground/suspensive.tsx +++ b/websites/visualization/components/forPlayground/suspensive.tsx @@ -1,7 +1,6 @@ import { Suspense } from '@suspensive/react' import { useSuspenseQuery } from '@suspensive/react-query' import { albums, Post, posts, todos } from './api' -import { Spinner } from '../uis' import { useIntersectionObserver } from './useIntersectionObserver' import { useEffect, useRef, useState } from 'react' @@ -32,7 +31,7 @@ const PostListItem = ({ post }: { post: Post }) => {
  • Title: {post.title}

    {isShow && ( - }> + )} diff --git a/websites/visualization/components/forPlayground/tanstack.tsx b/websites/visualization/components/forPlayground/tanstack.tsx index bbb335639..29741b309 100644 --- a/websites/visualization/components/forPlayground/tanstack.tsx +++ b/websites/visualization/components/forPlayground/tanstack.tsx @@ -3,12 +3,17 @@ import { albums, Post, posts, todos } from './api' import { Spinner } from '../uis' import { useEffect, useRef, useState } from 'react' import { useIntersectionObserver } from './useIntersectionObserver' +import { Delay } from '@suspensive/react' export const PostListTanStack = () => { const postsQuery = useQuery(['posts'], posts.getMany) if (postsQuery.isLoading) { - return + return ( + + + + ) } if (postsQuery.isError) { return <>error @@ -56,7 +61,11 @@ const PostContent = ({ id }: { id: number }) => { ) if (postQuery.isLoading || albumsQuery.isLoading || todosQuery.isLoading) { - return + return ( + + + + ) } if (postQuery.isError || albumsQuery.isError || todosQuery.isError) { return <>error diff --git a/websites/visualization/package.json b/websites/visualization/package.json index 8c92823f7..334632a11 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.8.1", - "@suspensive/react-query": "1.8.1", + "@suspensive/react": "*", + "@suspensive/react-query": "*", "@tanstack/react-query": "^4.16.1", "@tanstack/react-query-devtools": "^4.16.1", "axios": "^1.2.0", diff --git a/websites/visualization/pages/_app.tsx b/websites/visualization/pages/_app.tsx index b67448e8f..7a9305c5c 100644 --- a/websites/visualization/pages/_app.tsx +++ b/websites/visualization/pages/_app.tsx @@ -5,6 +5,8 @@ import Link from 'next/link' import styled from '@emotion/styled' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' +import { Delay, SuspensiveConfigs, SuspensiveProvider } from '@suspensive/react' +import { Spinner } from '../components/uis' const queryClient = new QueryClient({ defaultOptions: { @@ -14,22 +16,39 @@ const queryClient = new QueryClient({ }, }) +const suspensiveConfigs = new SuspensiveConfigs({ + defaultOptions: { + delay: { + ms: 1200, + }, + suspense: { + fallback: ( + + + + ), + }, + }, +}) + export default function MyApp({ Component, pageProps }: AppProps) { return ( - - - - - logo - {"Suspensive's Concepts Visualization"} - - - - - - - - + + + + + + logo + {"Suspensive's Concepts Visualization"} + + + + + + + + + ) } diff --git a/websites/visualization/pages/react-query/playground.tsx b/websites/visualization/pages/react-query/playground.tsx index 935197a1e..4872f6451 100644 --- a/websites/visualization/pages/react-query/playground.tsx +++ b/websites/visualization/pages/react-query/playground.tsx @@ -1,4 +1,3 @@ -import { Spinner } from '../../components/uis' import { Suspense } from '@suspensive/react' import { PostListTanStack, PostListSuspensive } from '../../components/forPlayground' @@ -26,7 +25,7 @@ const Page = () => ( >

    🔗 See code for @suspensive/react-query

    - }> +