From a04159838a6e3eec50111cbcffd115253dcf65f2 Mon Sep 17 00:00:00 2001 From: Jonghyeon Ko Date: Fri, 19 Jul 2024 03:09:32 +0900 Subject: [PATCH 1/2] feat(react): improve ClientOnly by useSyncExternalStore Co-authored-by: Dominik Dorfmeister <1021430+TkDodo@users.noreply.github.com> Co-authored-by: SeongMin Kim <86355699+Collection50@users.noreply.github.com> Co-authored-by: GwanSik Kim <39869096+gwansikk@users.noreply.github.com> --- .changeset/cyan-tigers-jam.md | 5 ++ packages/react/src/components/ClientOnly.tsx | 2 +- packages/react/src/hooks/useIsClient.spec.ts | 70 ++++++++++++++++++-- packages/react/src/hooks/useIsClient.ts | 17 ++--- 4 files changed, 76 insertions(+), 18 deletions(-) create mode 100644 .changeset/cyan-tigers-jam.md diff --git a/.changeset/cyan-tigers-jam.md b/.changeset/cyan-tigers-jam.md new file mode 100644 index 000000000..02898175c --- /dev/null +++ b/.changeset/cyan-tigers-jam.md @@ -0,0 +1,5 @@ +--- +"@suspensive/react": minor +--- + +feat(react): improve ClientOnly by useSyncExternalStore diff --git a/packages/react/src/components/ClientOnly.tsx b/packages/react/src/components/ClientOnly.tsx index 32429a3c7..154bcef31 100644 --- a/packages/react/src/components/ClientOnly.tsx +++ b/packages/react/src/components/ClientOnly.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from 'react' -import { useIsClient } from '../hooks/useIsClient' +import { useIsClient } from '../hooks' interface ClientOnlyProps { children: ReactNode diff --git a/packages/react/src/hooks/useIsClient.spec.ts b/packages/react/src/hooks/useIsClient.spec.ts index ce705580b..154e581fc 100644 --- a/packages/react/src/hooks/useIsClient.spec.ts +++ b/packages/react/src/hooks/useIsClient.spec.ts @@ -1,12 +1,70 @@ import { renderHook } from '@testing-library/react' -import { useIsClient } from '.' +import { useState, useSyncExternalStore } from 'react' +import { noop } from '../utils' +import { useIsClient, useIsomorphicLayoutEffect } from '.' describe('useIsClient', () => { - it('should return true when client side painting start', () => { - const { - result: { current: isClient }, - } = renderHook(() => useIsClient()) + it('should return true in client side rendering', () => { + const returnedFirst = renderHook(() => useIsClient()) + expect(returnedFirst.result.current).toBe(true) + returnedFirst.unmount() + const returnedSecond = renderHook(() => useIsClient()) + expect(returnedSecond.result.current).toBe(true) + }) + it("'s comparison with legacy useIsClientOnly", () => { + // check CSR environment first + expect(typeof document !== 'undefined').toBe(true) + + let renderCount = 0 + let chanceIsClientToBeFalse = false + + const emptySubscribe = () => noop + const getSnapshot = () => true + const getServerSnapshot = () => false + const useIsClient = () => { + const isClient = useSyncExternalStore(emptySubscribe, getSnapshot, getServerSnapshot) + renderCount++ + if (!isClient) { + chanceIsClientToBeFalse = true + } + return isClient + } + const { unmount } = renderHook(() => useIsClient()) + expect(renderCount).toBe(1) + expect(chanceIsClientToBeFalse).toBe(false) + unmount() + renderHook(() => useIsClient()) + expect(renderCount).toBe(2) + expect(chanceIsClientToBeFalse).toBe(false) + }) + it('improve legacy useIsClientOnly', () => { + // check CSR environment first + expect(typeof document !== 'undefined').toBe(true) + + let renderCount = 0 + let chanceIsClientToBeFalse = false + + /** + * @deprecated This is legacy useIsClientOnly + */ + const useIsClientLegacy = () => { + renderCount++ + const [isClient, setIsClient] = useState(false) + if (!isClient) { + chanceIsClientToBeFalse = true + } + useIsomorphicLayoutEffect(() => { + setIsClient(true) + }, []) - expect(isClient).toBe(true) + return isClient + } + const { unmount } = renderHook(() => useIsClientLegacy()) + expect(renderCount).toBe(2) + expect(chanceIsClientToBeFalse).toBe(true) + unmount() + renderHook(() => useIsClientLegacy()) + expect(renderCount).toBe(4) + expect(chanceIsClientToBeFalse).toBe(true) }) }) diff --git a/packages/react/src/hooks/useIsClient.ts b/packages/react/src/hooks/useIsClient.ts index 3329a6390..ecc823650 100644 --- a/packages/react/src/hooks/useIsClient.ts +++ b/packages/react/src/hooks/useIsClient.ts @@ -1,12 +1,7 @@ -import { useState } from 'react' -import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect' +import { useSyncExternalStore } from 'react' +import { noop } from '../utils' -export const useIsClient = () => { - const [isClient, setIsClient] = useState(false) - - useIsomorphicLayoutEffect(() => { - setIsClient(true) - }, []) - - return isClient -} +const emptySubscribe = () => noop +const getSnapshot = () => true +const getServerSnapshot = () => false +export const useIsClient = () => useSyncExternalStore(emptySubscribe, getSnapshot, getServerSnapshot) From 33e39b9f9f041abd40c52e6e6e53042c22c7adcc Mon Sep 17 00:00:00 2001 From: Jonghyeon Ko Date: Fri, 19 Jul 2024 16:40:10 +0900 Subject: [PATCH 2/2] chore: update --- packages/react/src/hooks/useIsClient.spec.ts | 50 ++++++-------------- 1 file changed, 15 insertions(+), 35 deletions(-) diff --git a/packages/react/src/hooks/useIsClient.spec.ts b/packages/react/src/hooks/useIsClient.spec.ts index 154e581fc..53b6719ce 100644 --- a/packages/react/src/hooks/useIsClient.spec.ts +++ b/packages/react/src/hooks/useIsClient.spec.ts @@ -1,7 +1,7 @@ +import { useIsomorphicLayoutEffect } from '@suspensive/utils' import { renderHook } from '@testing-library/react' -import { useState, useSyncExternalStore } from 'react' -import { noop } from '../utils' -import { useIsClient, useIsomorphicLayoutEffect } from '.' +import { useState } from 'react' +import { useIsClient } from './useIsClient' describe('useIsClient', () => { it('should return true in client side rendering', () => { @@ -14,57 +14,37 @@ describe('useIsClient', () => { it("'s comparison with legacy useIsClientOnly", () => { // check CSR environment first expect(typeof document !== 'undefined').toBe(true) - - let renderCount = 0 - let chanceIsClientToBeFalse = false - - const emptySubscribe = () => noop - const getSnapshot = () => true - const getServerSnapshot = () => false - const useIsClient = () => { - const isClient = useSyncExternalStore(emptySubscribe, getSnapshot, getServerSnapshot) - renderCount++ - if (!isClient) { - chanceIsClientToBeFalse = true - } - return isClient - } - const { unmount } = renderHook(() => useIsClient()) - expect(renderCount).toBe(1) - expect(chanceIsClientToBeFalse).toBe(false) + const mockUseIsClient = vi.fn(useIsClient) + const { unmount } = renderHook(() => mockUseIsClient()) + expect(mockUseIsClient).toBeCalledTimes(1) unmount() - renderHook(() => useIsClient()) - expect(renderCount).toBe(2) - expect(chanceIsClientToBeFalse).toBe(false) + renderHook(() => mockUseIsClient()) + expect(mockUseIsClient).toBeCalledTimes(2) }) it('improve legacy useIsClientOnly', () => { // check CSR environment first expect(typeof document !== 'undefined').toBe(true) - - let renderCount = 0 let chanceIsClientToBeFalse = false - /** * @deprecated This is legacy useIsClientOnly */ - const useIsClientLegacy = () => { - renderCount++ + const mockUseIsClientLegacy = vi.fn(() => { const [isClient, setIsClient] = useState(false) if (!isClient) { + // eslint-disable-next-line react-compiler/react-compiler chanceIsClientToBeFalse = true } useIsomorphicLayoutEffect(() => { setIsClient(true) }, []) - return isClient - } - const { unmount } = renderHook(() => useIsClientLegacy()) - expect(renderCount).toBe(2) + }) + const { unmount } = renderHook(() => mockUseIsClientLegacy()) + expect(mockUseIsClientLegacy).toBeCalledTimes(2) expect(chanceIsClientToBeFalse).toBe(true) unmount() - renderHook(() => useIsClientLegacy()) - expect(renderCount).toBe(4) + renderHook(() => mockUseIsClientLegacy()) + expect(mockUseIsClientLegacy).toBeCalledTimes(4) expect(chanceIsClientToBeFalse).toBe(true) }) })