Skip to content

Commit

Permalink
feat: implement useWindowSize hook
Browse files Browse the repository at this point in the history
Tracks window inner dimensions.
  • Loading branch information
xobotyi authored and JoeDuncko committed Mar 14, 2022
1 parent 6cd44ec commit 81819cf
Show file tree
Hide file tree
Showing 11 changed files with 277 additions and 84 deletions.
10 changes: 5 additions & 5 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: "Checkout"
uses: actions/checkout@v3
with:
ref: ${{ github.event.release.tag_name || 'master' }}
fetch-depth: 0

- uses: bahmutov/npm-install@v1
with:
Expand All @@ -34,7 +34,7 @@ jobs:
- name: "Checkout"
uses: actions/checkout@v3
with:
ref: ${{ github.event.release.tag_name || 'master' }}
fetch-depth: 0

- uses: bahmutov/npm-install@v1
with:
Expand All @@ -54,7 +54,7 @@ jobs:
- name: "Checkout"
uses: actions/checkout@v3
with:
ref: ${{ github.event.release.tag_name || 'master' }}
fetch-depth: 0

- uses: bahmutov/npm-install@v1
with:
Expand All @@ -71,7 +71,7 @@ jobs:
- name: "Checkout"
uses: actions/checkout@v3
with:
ref: ${{ github.event.release.tag_name || 'master' }}
fetch-depth: 0

- uses: bahmutov/npm-install@v1
with:
Expand Down Expand Up @@ -112,7 +112,7 @@ jobs:
- name: "Checkout"
uses: actions/checkout@v3
with:
ref: ${{ github.event.release.tag_name || 'master' }}
fetch-depth: 0

- uses: bahmutov/npm-install@v1
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/gh-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
- name: "Checkout"
uses: actions/checkout@v3
with:
ref: ${{ github.event.release.tag_name || 'master' }}
fetch-depth: 0

- uses: bahmutov/npm-install@v1
with:
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,5 @@ Coming from `react-use`? Check out our
— Subscribes an event listener to the target, and automatically unsubscribes it on unmount.
- [**`useKeyboardEvent`**](https://react-hookz.github.io/web/?path=/docs/dom-usekeyboardevent--example)
— Executes callback when keyboard event occurred on target.
- [**`useWindowSize`**](https://react-hookz.github.io/web/?path=/docs/dom-usewindowsize--example)
— Tracks window inner dimensions.
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export {
export { useClickOutside } from './useClickOutside/useClickOutside';
export { useDocumentTitle, IUseDocumentTitleOptions } from './useDocumentTitle/useDocumentTitle';
export { useEventListener } from './useEventListener/useEventListener';
export { useWindowSize, WindowSize } from './useWindowSize/useWindowSize';

export { truthyAndArrayPredicate, truthyOrArrayPredicate } from './util/const';

Expand Down
4 changes: 2 additions & 2 deletions src/useRafState/useRafState.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Dispatch, SetStateAction } from 'react';
import { useRafCallback, useSafeState, useUnmountEffect } from '../..';
import { useRafCallback, useSafeState, useUnmountEffect } from '..';

export function useRafState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
export function useRafState<S = undefined>(): [
Expand All @@ -13,7 +13,7 @@ export function useRafState<S = undefined>(): [
export function useRafState<S>(
initialState?: S | (() => S)
): [S | undefined, Dispatch<SetStateAction<S>>] {
const [state, innerSetState] = useSafeState(initialState);
const [state, innerSetState] = useSafeState<S | undefined>(initialState);

const [setState, cancelRaf] = useRafCallback(innerSetState);

Expand Down
17 changes: 17 additions & 0 deletions src/useWindowSize/__docs__/example.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as React from 'react';
import { useWindowSize } from '../..';

export const Example: React.FC = () => {
const size = useWindowSize();

return (
<div>
Window dimensions:
<pre>{JSON.stringify(size, null, 2)}</pre>
<blockquote>
Note: this hook is rendered within iframe which dimensions are smaller than your browser
window.
</blockquote>
</div>
);
};
47 changes: 47 additions & 0 deletions src/useWindowSize/__docs__/story.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks';
import { Example } from './example.stories';
import { ImportPath } from '../../storybookUtil/ImportPath';

<Meta title="DOM/useWindowSize" component={Example} />

# useWindowSize

Tracks window inner dimensions.

- Does not force `reflow` on every render - only on first mount and resizes.
- SSR-friendly - defaults to 0 during SSR and deferred measuring.
- Performant - uses single passive event listener for all instances.

#### Example

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

## Reference

```ts
export interface WindowSize {
width: number;
height: number;
}

export function useWindowSize(stateHook = useRafState, measureOnMount?: boolean): WindowSize;
```

#### Importing

<ImportPath />

#### Arguments

- **stateHook** `<S>(initialState?: S | (() => S)): [S | undefined, Dispatch<SetStateAction<S>>]` -
state hook that will be used to hold windows dimensions.
- **measureOnMount** `boolean` - whether windows size should be fetched during effects stage instead
of synchronous fetch. Set this parameter to `true` for SSR use-cases.

#### Return

- `object`
- **width** `number` - window's `innerWidth`
- **height** `number` - window's `innerHeight`
59 changes: 59 additions & 0 deletions src/useWindowSize/__tests__/dom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { act, renderHook } from '@testing-library/react-hooks/dom';
import { useState } from 'react';
import { useWindowSize, WindowSize } from '../..';

describe('useWindowSize', () => {
beforeEach(() => {
window.innerWidth = 100;
window.innerHeight = 100;
});

const triggerResize = (dimension: 'width' | 'height', value: number) => {
if (dimension === 'width') {
window.innerWidth = value;
} else if (dimension === 'height') {
window.innerHeight = value;
}

act(() => {
window.dispatchEvent(new Event('resize'));
});
};

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

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

it('should use provided state hook', () => {
const { result } = renderHook(() => useWindowSize(useState));

expect(result.current.width).toBe(100);
expect(result.current.height).toBe(100);
expect(result.all.length).toBe(1);

triggerResize('width', 200);
expect(result.current.width).toBe(200);
expect(result.current.height).toBe(100);
expect(result.all.length).toBe(2);

triggerResize('height', 200);
expect(result.current.width).toBe(200);
expect(result.current.height).toBe(200);
expect(result.all.length).toBe(3);
});

it('should delay measurement to effects stage if 2nd argument is `true`', () => {
const { result } = renderHook(() => useWindowSize(useState, true));

expect((result.all[0] as WindowSize).width).toBe(0);
expect((result.all[0] as WindowSize).height).toBe(0);

expect(result.current.width).toBe(100);
expect(result.current.height).toBe(100);
});
});
13 changes: 13 additions & 0 deletions src/useWindowSize/__tests__/ssr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { renderHook } from '@testing-library/react-hooks/server';
import { useWindowSize } from '../..';

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

it('should render', () => {
const { result } = renderHook(() => useWindowSize());
expect(result.error).toBeUndefined();
});
});
60 changes: 60 additions & 0 deletions src/useWindowSize/useWindowSize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useEffect } from 'react';
import { useFirstMountState, useMountEffect, useRafState } from '..';
import { isBrowser } from '../util/const';

export interface WindowSize {
width: number;
height: number;
}

const listeners = new Set<(size: WindowSize) => void>();

const callAllListeners = () => {
listeners.forEach((l) => {
l({
width: window.innerWidth,
height: window.innerHeight,
});
});
};

/**
* Tracks window inner dimensions.
*
* @param stateHook State hook that will be used internally. Default: `useRafState`.
* @param measureOnMount Perform size fetch during mount effect stage or synchronously with render.
*/
export function useWindowSize(stateHook = useRafState, measureOnMount?: boolean): WindowSize {
const isFirstMount = useFirstMountState();
const [size, setSize] = stateHook<WindowSize>({
width: isFirstMount && isBrowser && !measureOnMount ? window.innerWidth : 0,
height: isFirstMount && isBrowser && !measureOnMount ? window.innerHeight : 0,
});

useMountEffect(() => {
if (measureOnMount) {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
});

useEffect(() => {
if (listeners.size === 0) {
window.addEventListener('resize', callAllListeners, { passive: true });
}

listeners.add(setSize);

return () => {
listeners.delete(setSize);

if (listeners.size === 0) {
window.removeEventListener('resize', callAllListeners);
}
};
}, [setSize]);

return size;
}

0 comments on commit 81819cf

Please sign in to comment.