From 9b7a44813124a8b2dfcc8f1d5418ff20d1d8fcac Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Fri, 25 Apr 2025 17:04:37 +0900 Subject: [PATCH 1/5] feat(hooks): add 'useIsClient' --- src/hooks/useIsClient/index.ts | 1 + src/hooks/useIsClient/useIsClient.spec.tsx | 32 +++++++++++++++ src/hooks/useIsClient/useIsClient.ts | 46 ++++++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 src/hooks/useIsClient/index.ts create mode 100644 src/hooks/useIsClient/useIsClient.spec.tsx create mode 100644 src/hooks/useIsClient/useIsClient.ts diff --git a/src/hooks/useIsClient/index.ts b/src/hooks/useIsClient/index.ts new file mode 100644 index 00000000..b8d97825 --- /dev/null +++ b/src/hooks/useIsClient/index.ts @@ -0,0 +1 @@ +export { useIsClient } from './useIsClient.ts'; diff --git a/src/hooks/useIsClient/useIsClient.spec.tsx b/src/hooks/useIsClient/useIsClient.spec.tsx new file mode 100644 index 00000000..22c4eb25 --- /dev/null +++ b/src/hooks/useIsClient/useIsClient.spec.tsx @@ -0,0 +1,32 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; + +import { renderSSR } from '../../_internal/test-utils/renderSSR.tsx'; + +import { useIsClient } from './useIsClient.ts'; + +function TestComponent() { + const isClient = useIsClient(); + + return
{isClient ? 'Client-side' : 'Server-side'}
; +} + +describe('useIsClient', () => { + it('should render "Server-side" text when rendered in a server environment (SSR)', () => { + renderSSR.serverOnly(() => ); + + expect(screen.getByText('Server-side')).toBeInTheDocument(); + }); + + it('should update to "Client-side" text after hydration on the client', async () => { + await renderSSR(() => ); + + expect(screen.getByText('Client-side')).toBeInTheDocument(); + }); + + it('should render "Client-side" text when mounted directly on the client', async () => { + render(); + + expect(screen.getByText('Client-side')).toBeInTheDocument(); + }); +}); diff --git a/src/hooks/useIsClient/useIsClient.ts b/src/hooks/useIsClient/useIsClient.ts new file mode 100644 index 00000000..5b0af23c --- /dev/null +++ b/src/hooks/useIsClient/useIsClient.ts @@ -0,0 +1,46 @@ +import { useEffect, useState } from 'react'; + +/** + * @description + * `useIsClient` is a React hook that returns `true` only in the client-side environment. + * It is primarily used to differentiate between client-side and server-side rendering (SSR). + * The state is set to `true` only after the component is mounted in the client-side environment. + * + * @returns {boolean} Returns `true` in a client-side environment, and `false` otherwise. + * + * @example + * function MyComponent() { + * const isClient = useIsClient(); + * + * if (!isClient) { + * return
Loading...
; // Rendered on the server side + * } + * + * return
Client-side rendered content
; // Rendered on the client side + * } + * + * @example + * function MapComponent() { + * const isClient = useIsClient(); + * + * if (!isClient) return null; + * + * return
; + * } + * + * @example + * function UserTheme() { + * const isClient = useIsClient(); + * + * const theme = isClient ? localStorage.getItem('theme') : 'light'; + * + * return
Current theme: {theme}
; + * } + */ +export function useIsClient() { + const [isClient, setIsClient] = useState(false); + + useEffect(() => setIsClient(true), []); + + return isClient; +} From d906cc21b75e2449e5abec61e338fe3462fc3588 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Fri, 25 Apr 2025 17:16:04 +0900 Subject: [PATCH 2/5] feat(hooks/useIsClient): rename example functions for better clarity --- src/hooks/useIsClient/useIsClient.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hooks/useIsClient/useIsClient.ts b/src/hooks/useIsClient/useIsClient.ts index 5b0af23c..ec01cfc1 100644 --- a/src/hooks/useIsClient/useIsClient.ts +++ b/src/hooks/useIsClient/useIsClient.ts @@ -9,7 +9,7 @@ import { useEffect, useState } from 'react'; * @returns {boolean} Returns `true` in a client-side environment, and `false` otherwise. * * @example - * function MyComponent() { + * function ClientSideContent() { * const isClient = useIsClient(); * * if (!isClient) { @@ -20,7 +20,7 @@ import { useEffect, useState } from 'react'; * } * * @example - * function MapComponent() { + * function ClientOnlyMap() { * const isClient = useIsClient(); * * if (!isClient) return null; @@ -29,7 +29,7 @@ import { useEffect, useState } from 'react'; * } * * @example - * function UserTheme() { + * function ClientTheme() { * const isClient = useIsClient(); * * const theme = isClient ? localStorage.getItem('theme') : 'light'; From 3d22b831f0c3a702cf5ef8ce658e85f164a31130 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Thu, 8 May 2025 10:38:27 +0900 Subject: [PATCH 3/5] feat(src): add 'useIsClient' in 'index.ts' --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index 5a5911dc..d48d0431 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,7 @@ export { useImpressionRef } from './hooks/useImpressionRef/index.ts'; export { useInputState } from './hooks/useInputState/index.ts'; export { useIntersectionObserver } from './hooks/useIntersectionObserver/index.ts'; export { useInterval } from './hooks/useInterval/index.ts'; +export { useIsClient } from './hooks/useIsClient/index.ts'; export { useIsomorphicLayoutEffect } from './hooks/useIsomorphicLayoutEffect/index.ts'; export { useLoading } from './hooks/useLoading/index.ts'; export { useOutsideClickEffect } from './hooks/useOutsideClickEffect/index.ts'; From 90781a2f885ad5f5e131c9a2694b56055a7393cd Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Wed, 4 Mar 2026 01:01:16 +0900 Subject: [PATCH 4/5] refactor(useIsClient): migrate to monorepo structure and add bilingual docs --- .../core/src}/hooks/useIsClient/index.ts | 0 .../src/hooks/useIsClient/ko/useIsClient.md | 33 +++++++++++++++++++ .../core/src/hooks/useIsClient/useIsClient.md | 33 +++++++++++++++++++ .../src/hooks/useIsClient/useIsClient.spec.ts | 19 +++++++++++ .../src}/hooks/useIsClient/useIsClient.ts | 4 ++- src/hooks/useIsClient/useIsClient.spec.tsx | 32 ------------------ 6 files changed, 88 insertions(+), 33 deletions(-) rename {src => packages/core/src}/hooks/useIsClient/index.ts (100%) create mode 100644 packages/core/src/hooks/useIsClient/ko/useIsClient.md create mode 100644 packages/core/src/hooks/useIsClient/useIsClient.md create mode 100644 packages/core/src/hooks/useIsClient/useIsClient.spec.ts rename {src => packages/core/src}/hooks/useIsClient/useIsClient.ts (94%) delete mode 100644 src/hooks/useIsClient/useIsClient.spec.tsx diff --git a/src/hooks/useIsClient/index.ts b/packages/core/src/hooks/useIsClient/index.ts similarity index 100% rename from src/hooks/useIsClient/index.ts rename to packages/core/src/hooks/useIsClient/index.ts diff --git a/packages/core/src/hooks/useIsClient/ko/useIsClient.md b/packages/core/src/hooks/useIsClient/ko/useIsClient.md new file mode 100644 index 00000000..34eef9a5 --- /dev/null +++ b/packages/core/src/hooks/useIsClient/ko/useIsClient.md @@ -0,0 +1,33 @@ +# useIsClient + +`useIsClient`는 클라이언트 환경에서만 `true`를 반환하는 리액트 훅이에요. 주로 클라이언트와 서버 사이드 렌더링(SSR)을 구분하기 위해 사용돼요. 컴포넌트가 클라이언트 환경에서 마운트된 후에만 `true`로 설정돼요. + +## 인터페이스 + +```ts +function useIsClient(): boolean; +``` + +### 반환 값 + + + +## 예시 + +```tsx +import { useIsClient } from 'react-simplikit'; + +function ClientSideContent() { + const isClient = useIsClient(); + + if (!isClient) { + return
Loading...
; + } + + return
Client-side rendered content
; +} +``` diff --git a/packages/core/src/hooks/useIsClient/useIsClient.md b/packages/core/src/hooks/useIsClient/useIsClient.md new file mode 100644 index 00000000..561bc22f --- /dev/null +++ b/packages/core/src/hooks/useIsClient/useIsClient.md @@ -0,0 +1,33 @@ +# useIsClient + +`useIsClient` is a React hook that returns `true` only in the client-side environment. It is primarily used to differentiate between client-side and server-side rendering (SSR). The state is set to `true` only after the component is mounted in the client-side environment. + +## Interface + +```ts +function useIsClient(): boolean; +``` + +### Return Value + + + +## Example + +```tsx +import { useIsClient } from 'react-simplikit'; + +function ClientSideContent() { + const isClient = useIsClient(); + + if (!isClient) { + return
Loading...
; + } + + return
Client-side rendered content
; +} +``` diff --git a/packages/core/src/hooks/useIsClient/useIsClient.spec.ts b/packages/core/src/hooks/useIsClient/useIsClient.spec.ts new file mode 100644 index 00000000..ad3245cc --- /dev/null +++ b/packages/core/src/hooks/useIsClient/useIsClient.spec.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest'; + +import { renderHookSSR } from '../../_internal/test-utils/renderHookSSR.tsx'; + +import { useIsClient } from './useIsClient.ts'; + +describe('useIsClient', () => { + it('is safe on server side rendering', async () => { + const result = renderHookSSR.serverOnly(() => useIsClient()); + + expect(result.current).toBe(false); + }); + + it('should return true after hydration on the client', async () => { + const { result } = await renderHookSSR(() => useIsClient()); + + expect(result.current).toBe(true); + }); +}); diff --git a/src/hooks/useIsClient/useIsClient.ts b/packages/core/src/hooks/useIsClient/useIsClient.ts similarity index 94% rename from src/hooks/useIsClient/useIsClient.ts rename to packages/core/src/hooks/useIsClient/useIsClient.ts index ec01cfc1..ea350e78 100644 --- a/src/hooks/useIsClient/useIsClient.ts +++ b/packages/core/src/hooks/useIsClient/useIsClient.ts @@ -40,7 +40,9 @@ import { useEffect, useState } from 'react'; export function useIsClient() { const [isClient, setIsClient] = useState(false); - useEffect(() => setIsClient(true), []); + useEffect(function syncClientState() { + setIsClient(true); + }, []); return isClient; } diff --git a/src/hooks/useIsClient/useIsClient.spec.tsx b/src/hooks/useIsClient/useIsClient.spec.tsx deleted file mode 100644 index 22c4eb25..00000000 --- a/src/hooks/useIsClient/useIsClient.spec.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import { describe, expect, it } from 'vitest'; - -import { renderSSR } from '../../_internal/test-utils/renderSSR.tsx'; - -import { useIsClient } from './useIsClient.ts'; - -function TestComponent() { - const isClient = useIsClient(); - - return
{isClient ? 'Client-side' : 'Server-side'}
; -} - -describe('useIsClient', () => { - it('should render "Server-side" text when rendered in a server environment (SSR)', () => { - renderSSR.serverOnly(() => ); - - expect(screen.getByText('Server-side')).toBeInTheDocument(); - }); - - it('should update to "Client-side" text after hydration on the client', async () => { - await renderSSR(() => ); - - expect(screen.getByText('Client-side')).toBeInTheDocument(); - }); - - it('should render "Client-side" text when mounted directly on the client', async () => { - render(); - - expect(screen.getByText('Client-side')).toBeInTheDocument(); - }); -}); From d81ab5c0c00f6052e65fc693e062549943521073 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Wed, 4 Mar 2026 01:05:15 +0900 Subject: [PATCH 5/5] chore(changeset): add patch changeset for 'useIsClient' --- .changeset/dirty-coats-fold.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/dirty-coats-fold.md diff --git a/.changeset/dirty-coats-fold.md b/.changeset/dirty-coats-fold.md new file mode 100644 index 00000000..bbe7d7c2 --- /dev/null +++ b/.changeset/dirty-coats-fold.md @@ -0,0 +1,5 @@ +--- +'react-simplikit': patch +--- + +feat(core/hooks): add 'useIsClient' hook