Skip to content

Commit

Permalink
feat: new hook useTitle (#68)
Browse files Browse the repository at this point in the history
* feat: new hook `useTitle`

* fix: index.ts exports

* docs: add clarification about not working story
  • Loading branch information
xobotyi authored May 25, 2021
1 parent 6efe0f4 commit 84e4cbf
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 0 deletions.
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ export { useSessionStorageValue } from './useSessionStorageValue';

// Sensor
export { useResizeObserver, IUseResizeObserverCallback } from './useResizeObserver';

// Dom
export { useTitle, IUseTitleOptions } from './useTitle';
40 changes: 40 additions & 0 deletions src/useTitle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useEffect, useRef } from 'react';
import { isBrowser } from './util/const';
import { useUnmountEffect } from './useUnmountEffect';
import { useSyncedRef } from './useSyncedRef';

export interface IUseTitleOptions {
/**
* Function that processes title, useful to prefix or suffix the title.
* @param title
*/
wrapper: (title: string) => string;

/**
* Restore title that was before component mount on unmount.
*/
restoreOnUnmount: boolean;
}

/**
* Sets title of the page.
*
* @param title Title to set, if wrapper option is set, it will be passed through wrapper function.
* @param options Options object.
*/
export function useTitle(title: string, options: Partial<IUseTitleOptions> = {}): void {
const titleRef = useRef(isBrowser ? document.title : '');
const optionsRef = useSyncedRef(options);

// it is safe not to check isBrowser here, as effects are not invoked in SSR
useEffect(() => {
document.title = options.wrapper ? options.wrapper(title) : title;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [title, options.wrapper]);

useUnmountEffect(() => {
if (optionsRef.current.restoreOnUnmount) {
document.title = titleRef.current;
}
});
}
26 changes: 26 additions & 0 deletions stories/Dom/useTitle.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';
import { useTitle, useToggle } from '../../src';

export const Example: React.FC = () => {
const [mounted, toggleMounted] = useToggle(false);

const titleWrapper = (title: string) => `@react-hookz/web is ${title}`;

const ChildComponent: React.FC = () => {
useTitle('awesome!', {
wrapper: titleWrapper,
restoreOnUnmount: true,
});

return <div>Child component is mounted</div>;
};

return (
<div>
While child component is mounted, document will have custom title and it will be reset to
default when component is unmounted. <br />
<button onClick={() => toggleMounted()}>{mounted ? 'Unmount' : 'Mount'} component</button>
{mounted && <ChildComponent />}
</div>
);
};
42 changes: 42 additions & 0 deletions stories/Dom/useTitle.story.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks';
import { Example } from './useTitle.stories';

<Meta title="Dom/useTitle" component={Example} />

# useTitle

Sets title of the page.

> If `restoreOnUnmount` option is true the hook will restore title to the state it was on component
> mount.
> `wrapper` option is useful to prefix or suffix the title all over the page.
##### Example

<Canvas>
<Story inline story={Example} />
</Canvas>

> Due to storybook structure, above example is changing the title of an iframe but not the browser
> window. We are working on solving the problem.
## Reference

```ts
export interface IUseTitleOptions {
wrapper: (title: string) => string;
restoreOnUnmount: boolean;
}

function useTitle(title: string, options: Partial<IUseTitleOptions> = {}): void;
```

#### Arguments

- **title** _`string`_ - title string to set.
- **options** _`object`_ - Hook options:
- **wrapper** _`(title: string) => string`_ _(default: undefined)_ - Function that receives new
title and returns wrapped title.
- **restoreOnUnmount** _`boolean`_ _(default: undefined)_ - Whether to restore page title on
component unmount.
57 changes: 57 additions & 0 deletions tests/dom/useTitle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { renderHook } from '@testing-library/react-hooks/dom';
import { useTitle } from '../../src';

describe('useTitle', () => {
it('should be defined', () => {
expect(useTitle).toBeDefined();
});

it('should render', () => {
const { result } = renderHook(() => useTitle('some title'));
expect(result.error).toBeUndefined();
});

it('should unmount without errors', () => {
const { unmount, result } = renderHook(() =>
useTitle('some title', { restoreOnUnmount: true })
);

unmount();
expect(result.error).toBeUndefined();
});

it('should set given string to the document title', () => {
renderHook(() => useTitle('foo'));
expect(document.title).toBe('foo');
});

it('should pass title through wrapper', () => {
const wrapper = (str: string) => `${str} bar`;
renderHook(() => useTitle('foo', { wrapper }));
expect(document.title).toBe('foo bar');
});

it('should update title if title or wrapper changed', () => {
let wrapperSpy = (str: string) => `${str} bar`;
const { rerender } = renderHook(({ title, wrapper }) => useTitle(title, { wrapper }), {
initialProps: { title: 'foo', wrapper: wrapperSpy },
});
expect(document.title).toBe('foo bar');

rerender({ title: 'bar', wrapper: wrapperSpy });
expect(document.title).toBe('bar bar');

wrapperSpy = (str: string) => `${str} baz`;
rerender({ title: 'bar', wrapper: wrapperSpy });
expect(document.title).toBe('bar baz');
});

it('should set previous title in case `restoreOnUnmount` options is truthy', () => {
document.title = 'bar';
const { unmount } = renderHook(() => useTitle('foo', { restoreOnUnmount: true }));
expect(document.title).toBe('foo');

unmount();
expect(document.title).toBe('bar');
});
});
22 changes: 22 additions & 0 deletions tests/ssr/useTitle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { renderHook } from '@testing-library/react-hooks/server';
import { useTitle } from '../../src';

describe('useTitle', () => {
it('should be defined', () => {
expect(useTitle).toBeDefined();
});

it('should render', () => {
const { result } = renderHook(() => useTitle('some title'));
expect(result.error).toBeUndefined();
});

it('should unmount without errors', () => {
const { unmount, result } = renderHook(() =>
useTitle('some title', { restoreOnUnmount: true })
);

unmount();
expect(result.error).toBeUndefined();
});
});

0 comments on commit 84e4cbf

Please sign in to comment.