diff --git a/src/hooks/useIsClient/index.ts b/src/hooks/useIsClient/index.ts new file mode 100644 index 0000000..b8d9782 --- /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 0000000..22c4eb2 --- /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 0000000..ec01cfc --- /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 ClientSideContent() { + * const isClient = useIsClient(); + * + * if (!isClient) { + * return
Loading...
; // Rendered on the server side + * } + * + * return
Client-side rendered content
; // Rendered on the client side + * } + * + * @example + * function ClientOnlyMap() { + * const isClient = useIsClient(); + * + * if (!isClient) return null; + * + * return
; + * } + * + * @example + * function ClientTheme() { + * 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; +} diff --git a/src/index.ts b/src/index.ts index 5a5911d..d48d043 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';