diff --git a/.changeset/tall-steaks-melt.md b/.changeset/tall-steaks-melt.md new file mode 100644 index 000000000..f4a212f4d --- /dev/null +++ b/.changeset/tall-steaks-melt.md @@ -0,0 +1,5 @@ +--- +'@modern-kit/react': minor +--- + +feat(react): useDocumentTitle 커스텀 훅 추가 - @ssi02014 diff --git a/docs/docs/react/hooks/useDoumentTitle.mdx b/docs/docs/react/hooks/useDoumentTitle.mdx new file mode 100644 index 000000000..2fd36b858 --- /dev/null +++ b/docs/docs/react/hooks/useDoumentTitle.mdx @@ -0,0 +1,82 @@ +import { useDocumentTitle } from '@modern-kit/react'; +import { useState } from 'react'; + +# useDocumentTitle + +`SEO`와는 관계 없이 `document.title`을 동적으로 변경시켜주는 커스텀 훅입니다. + +`preserveTitleOnUnmount` 옵션을 `true`로 준다면 `unmount` 시에 변경 된 타이틀로 유지할 수 있습니다. + +
+ +## Code +[🔗 실제 구현 코드 확인](https://github.com/modern-agile-team/modern-kit/blob/main/packages/react/src/hooks/useDocumentTitle/index.ts) + +## Interface +```ts title="typescript" +interface UseDocumentTitleOption { + preserveTitleOnUnmount?: boolean; // default: false +} + +const useDocumentTitle: ( + title: string, + { preserveTitleOnUnmount }?: UseDocumentTitleOption +) => void; +``` + +## Usage +```tsx title="typescript" +import { useState } from 'react'; +import { useDocumentTitle } from '@modern-kit/react'; + +const Example = () => { + const [title, setTitle] = useState('useDocumentTitle'); + const [inputValue, setInputValue] = useState(''); + + const handleChangeTitle = () => { + setTitle(inputValue); + alert('타이틀이 변경됐습니다.'); + }; + + useDocumentTitle(title, { + preserveTitleOnUnmount: false, // default: false + }); + + return ( +
+ setInputValue(e.target.value)} + /> + +
+ ); +}; +``` + +## Example + +export const Example = () => { + const [title, setTitle] = useState('useDocumentTitle'); + const [inputValue, setInputValue] = useState(''); + const handleChangeTitle = () => { + setTitle(inputValue); + alert('타이틀이 변경됐습니다.'); + }; + useDocumentTitle(title, { + preserveTitleOnUnmount: false, // default: false + }); + return ( +
+ setInputValue(e.target.value)} + /> + +
+ ); +}; + + \ No newline at end of file diff --git a/packages/react/src/hooks/index.ts b/packages/react/src/hooks/index.ts index ce3f0002a..528b9ba3b 100644 --- a/packages/react/src/hooks/index.ts +++ b/packages/react/src/hooks/index.ts @@ -3,6 +3,7 @@ export * from './useAsyncProcessQueue'; export * from './useBlockPromiseMultipleClick'; export * from './useClipboard'; export * from './useDebounce'; +export * from './useDocumentTitle'; export * from './useFileReader'; export * from './useForceUpdate'; export * from './useImageStatus'; diff --git a/packages/react/src/hooks/useDocumentTitle/index.ts b/packages/react/src/hooks/useDocumentTitle/index.ts new file mode 100644 index 000000000..3acd6d70d --- /dev/null +++ b/packages/react/src/hooks/useDocumentTitle/index.ts @@ -0,0 +1,21 @@ +import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect'; + +interface UseDocumentTitleOption { + preserveTitleOnUnmount?: boolean; +} + +export const useDocumentTitle = ( + title: string, + { preserveTitleOnUnmount = false }: UseDocumentTitleOption = {} +) => { + useIsomorphicLayoutEffect(() => { + const prevTitle = document.title; + document.title = title; + + return () => { + if (!preserveTitleOnUnmount) { + document.title = prevTitle; + } + }; + }, [title, preserveTitleOnUnmount]); +}; diff --git a/packages/react/src/hooks/useDocumentTitle/useDoucmentTitle.spec.ts b/packages/react/src/hooks/useDocumentTitle/useDoucmentTitle.spec.ts new file mode 100644 index 000000000..1698ac2be --- /dev/null +++ b/packages/react/src/hooks/useDocumentTitle/useDoucmentTitle.spec.ts @@ -0,0 +1,55 @@ +import { renderHook } from '@testing-library/react'; +import { useDocumentTitle } from '.'; + +const ORIGIN_TITLE = 'origin title'; +const FIRST_CHANGE_TITLE = 'first change title'; +const SECOND_CHANGE_TITLE = 'second change title'; + +beforeEach(() => { + document.title = ORIGIN_TITLE; +}); + +describe('useDocumentTitle', () => { + it('should update the document title', () => { + const { rerender, unmount } = renderHook( + ({ title }) => useDocumentTitle(title), + { + initialProps: { title: FIRST_CHANGE_TITLE }, + } + ); + + expect(document.title).toBe(FIRST_CHANGE_TITLE); + + rerender({ title: SECOND_CHANGE_TITLE }); + + expect(document.title).toBe(SECOND_CHANGE_TITLE); + + unmount(); + + expect(document.title).toBe(ORIGIN_TITLE); + }); + + it('should revert to the original title on unmount if preserveTitleOnUnmount is false', () => { + const { unmount } = renderHook(() => + useDocumentTitle(FIRST_CHANGE_TITLE, { preserveTitleOnUnmount: false }) + ); + + expect(document.title).toBe(FIRST_CHANGE_TITLE); + + unmount(); + + expect(document.title).toBe(ORIGIN_TITLE); + }); + + it('should retain the changed title on unmount if preserveTitleOnUnmount is true', () => { + const { unmount } = renderHook(() => + useDocumentTitle(FIRST_CHANGE_TITLE, { preserveTitleOnUnmount: true }) + ); + + expect(document.title).toBe(FIRST_CHANGE_TITLE); + + unmount(); + + expect(document.title).toBe(FIRST_CHANGE_TITLE); + }); +});