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
-}