diff --git a/.changeset/tame-shrimps-rescue.md b/.changeset/tame-shrimps-rescue.md new file mode 100644 index 000000000..734ff90ae --- /dev/null +++ b/.changeset/tame-shrimps-rescue.md @@ -0,0 +1,5 @@ +--- +"@suspensive/react": patch +--- + +fix(react): convert Suspense.CSROnly's useEffect into useIsomorphicLayoutEffect to prevent unnecessary layout shift diff --git a/packages/react/src/Suspense.tsx b/packages/react/src/Suspense.tsx index 0a5ca8f10..9d1a998bb 100644 --- a/packages/react/src/Suspense.tsx +++ b/packages/react/src/Suspense.tsx @@ -1,6 +1,6 @@ import type { ComponentProps, ComponentType, ReactNode, SuspenseProps as ReactSuspenseProps } from 'react' import { Suspense as ReactSuspense, createContext, useContext } from 'react' -import { useIsMounted } from './hooks' +import { useIsClient } from './hooks' import type { PropsWithoutChildren } from './types' export type SuspenseProps = ReactSuspenseProps @@ -21,10 +21,10 @@ if (process.env.NODE_ENV !== 'production') { DefaultSuspense.displayName = 'Suspense' } const CSROnlySuspense = (props: SuspenseProps) => { - const isMounted = useIsMounted() + const isClient = useIsClient() const fallback = useFallbackWithContext(props.fallback) - return isMounted ? : <>{fallback} + return isClient ? : <>{fallback} } if (process.env.NODE_ENV !== 'production') { CSROnlySuspense.displayName = 'Suspense.CSROnly' diff --git a/packages/react/src/hooks/index.ts b/packages/react/src/hooks/index.ts index 5e354cd50..825ac85ad 100644 --- a/packages/react/src/hooks/index.ts +++ b/packages/react/src/hooks/index.ts @@ -1,5 +1,5 @@ export { useIsChanged } from './useIsChanged' -export { useIsMounted } from './useIsMounted' +export { useIsClient } from './useIsClient' export { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect' export { useKey } from './useKey' export { usePrevious } from './usePrevious' diff --git a/packages/react/src/hooks/useIsClient.spec.ts b/packages/react/src/hooks/useIsClient.spec.ts new file mode 100644 index 000000000..ce705580b --- /dev/null +++ b/packages/react/src/hooks/useIsClient.spec.ts @@ -0,0 +1,12 @@ +import { renderHook } from '@testing-library/react' +import { useIsClient } from '.' + +describe('useIsClient', () => { + it('should return true when client side painting start', () => { + const { + result: { current: isClient }, + } = renderHook(() => useIsClient()) + + expect(isClient).toBe(true) + }) +}) diff --git a/packages/react/src/hooks/useIsClient.ts b/packages/react/src/hooks/useIsClient.ts new file mode 100644 index 000000000..3329a6390 --- /dev/null +++ b/packages/react/src/hooks/useIsClient.ts @@ -0,0 +1,12 @@ +import { useState } from 'react' +import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect' + +export const useIsClient = () => { + const [isClient, setIsClient] = useState(false) + + useIsomorphicLayoutEffect(() => { + setIsClient(true) + }, []) + + return isClient +} diff --git a/packages/react/src/hooks/useIsMounted.spec.ts b/packages/react/src/hooks/useIsMounted.spec.ts deleted file mode 100644 index 5954eaacb..000000000 --- a/packages/react/src/hooks/useIsMounted.spec.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { renderHook } from '@testing-library/react' -import { useIsMounted } from '.' - -describe('useIsMounted', () => { - it('should return true when component is mounted', () => { - const { - result: { current: isMounted }, - } = renderHook(() => useIsMounted()) - - expect(isMounted).toBe(true) - }) -}) diff --git a/packages/react/src/hooks/useIsMounted.ts b/packages/react/src/hooks/useIsMounted.ts deleted file mode 100644 index 3ce612fe5..000000000 --- a/packages/react/src/hooks/useIsMounted.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useEffect, useState } from 'react' - -export const useIsMounted = () => { - const [isMounted, setIsMounted] = useState(false) - - useEffect(() => { - setIsMounted(true) - }, []) - - return isMounted -}