├── .babelrc
├── .gitattributes
├── .github
└── workflows
│ └── node.js.yml
├── .gitignore
├── LICENSE
├── README.md
├── babel.config.testing.js
├── doc
└── use-viewport-sizes.gif
├── jest.config.js
├── package-lock.json
├── package.json
├── src
├── index.d.ts
├── index.js
└── index.test.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "targets": {
7 | "browsers": "last 2 versions, not ie 11",
8 | "node": "20"
9 | },
10 | "exclude": [
11 | "@babel/plugin-proposal-optional-chaining",
12 | "transform-destructuring",
13 | "transform-typeof-symbol",
14 | "transform-parameters",
15 | "transform-for-of",
16 | "transform-regenerator",
17 | "transform-object-rest-spread"
18 | ]
19 | }
20 | ]
21 | ],
22 | "plugins": [
23 | [
24 | "@babel/plugin-transform-runtime",
25 | {
26 | "helpers": true,
27 | "regenerator": false
28 | }
29 | ]
30 | ]
31 | }
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=crlf
--------------------------------------------------------------------------------
/.github/workflows/node.js.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
3 |
4 | name: Tests/CI
5 |
6 | on:
7 | push:
8 | branches: [ "master" ]
9 | pull_request:
10 | branches: [ "master" ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | matrix:
19 | node-version: [18.x, 20.x]
20 |
21 | steps:
22 | - uses: actions/checkout@v3
23 | - name: Use Node.js ${{ matrix.node-version }}
24 | uses: actions/setup-node@v3
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 | cache: 'npm'
28 | - run: npm ci
29 | - run: npm run build --if-present
30 | - run: npm test
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | media
3 | build
4 | coverage
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Robert Concepcion III
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # use-viewport-sizes #
2 |
3 | [](https://www.npmjs.com/package/use-viewport-sizes) []() [](https://github.com/rob2d/use-viewport-sizes/issues)
4 | 
5 | [](https://github.com/rob2d/use-viewport-sizes/blob/master/LICENSE)
6 |
7 | A lightweight, TypeScript-compatible React hook for tracking viewport sizes in your components. Includes optional debounce, throttle, and custom memoization for optimized rendering.
8 |
9 | ## Table of Contents
10 | - [Installation](#installation)
11 | - [Benefits](#benefits)
12 | - [Usage](#usage)
13 | - [Basic Use-case](#basic-use-case)
14 | - [Measure/Update only on one dimension](#measureupdate-only-on-one-dimension)
15 | - [With Throttling](#with-throttling)
16 | - [With Debouncing](#with-debouncing)
17 | - [Only update vpW/vpH passed on specific conditions](#only-update-vpwvph-passed-on-specific-conditions)
18 | - [Support](#support)
19 | - [License](#license)
20 |
21 | ## Installation ##
22 |
23 | ```
24 | npm install -D use-viewport-sizes
25 | ```
26 |
27 | ## Benefits ##
28 | - extremely lightweight and zero dependencies -- adds **2.38kb** pre gzip, and **1.09kb** after gzip.
29 | - only one `window.onresize` handler used to subscribe to any changes in an unlimited number of components no matter the use-cases.
30 | - optional debounce to delay updates until user stops dragging their window for a moment; this can make expensive components with size-dependent calculations run much faster and your app feel smoother.
31 | - debouncing does not create new handlers or waste re-renders in your component; the results are also pooled from only one resize result.
32 | - optional hash function to update component subtree only at points you would like to.
33 | - supports lazy loaded components and SSR out of the box.
34 | - compatible with React v16 | v17 | v18 | v19
35 |
36 |
37 | ## Usage ##
38 |
39 | ### **See it in Action** ###
40 |
41 |
42 |
43 |
44 | [CodeSandbox IDE](https://codesandbox.io/s/react-hooks-viewport-sizes-demo-forked-i8urr)
45 |
46 |
47 |
48 | ### **Basic Use-case**
49 | *registers dimension changes on every resize event immediately*
50 |
51 | ```js
52 | import useViewportSizes from 'use-viewport-sizes'
53 |
54 | function MyComponent(props) {
55 | const [vpWidth, vpHeight] = useViewportSizes();
56 |
57 | // ...renderLogic
58 | }
59 | ```
60 |
61 | ### **Measure/Update only on one dimension**
62 |
63 | If passed `options.dimension` as `w` or `h`, only the viewport width or height will be
64 | measured and observed for updates.
65 | The only dimension returned in the return array value will be the width or height, according
66 | to what was passed.
67 |
68 | ```js
69 | import useViewportSizes from 'use-viewport-sizes';
70 |
71 | function MyComponent(props) {
72 | const [vpHeight] = useViewportSizes({ dimension: 'h' });
73 |
74 | // ...renderLogic
75 | }
76 | ```
77 |
78 | ### **With Throttling**
79 |
80 | If passed `options.throttleTimeout`, or options are entered as a `Number`, dimension changes
81 | are registered on a throttled basis e.g. with a maximum frequency.
82 |
83 | This is useful for listening to expensive components such as data grids which may be too
84 | expensive to re-render during window resize dragging.
85 |
86 | ```js
87 | import useViewportSizes from 'use-viewport-sizes';
88 |
89 | function MyExpensivelyRenderedComponent(props) {
90 | // throttled by 1s between updates
91 | const [vpWidth, vpHeight] = useViewportSizes({ throttleTimeout: 1000 });
92 |
93 | // ...renderLogic
94 | }
95 | ```
96 |
97 | ### **With Debouncing**
98 |
99 | If passed `options.debounceTimeout`, dimension changes are registered only when a user stops dragging/resizing the window for a specified number of miliseconds. This is an alternative behavior to `throttleTimeout` where it may be less
100 | important to update viewport the entire way that a user is resizing.
101 |
102 | ```js
103 | import useViewportSizes from 'use-viewport-sizes';
104 |
105 | function MyExpensivelyRenderedComponent(props) {
106 | // debounced by 1s between updates
107 | const [vpWidth, vpHeight] = useViewportSizes({ debounceTimeout: 1000 });
108 |
109 | // ...renderLogic
110 | }
111 | ```
112 |
113 | ### **Only update vpW/vpH passed on specific conditions**
114 | If passed an `options.hasher` function, this will be used to calculate a hash that only updates the viewport when the calculation changes. In the example here, we are using it to detect when we have a breakpoint change which may change how a component is rendered if this is not fully possible or inconvenient via CSS `@media` queries. The hash will also be available as the 3rd value returned from the hook for convenience.
115 |
116 | ```js
117 | import useViewportSizes from 'use-viewport-sizes';
118 |
119 | function getBreakpointHash({ vpW, vpH }) {
120 | if(vpW <= 240) { return 'xs' }
121 | if(vpW <= 320) { return 'sm' }
122 | else if(vpW <= 640) { return 'md' }
123 | return 'lg';
124 | }
125 |
126 | function MyBreakpointBehaviorComponent() {
127 | const [vpW, vpH] = useViewportSizes({ hasher: getBreakpointHash });
128 |
129 | // do-something in render and add new update for vpW,
130 | // vpH in this component's subtree only when a breakpoint
131 | // hash updates
132 | }
133 | ```
134 |
135 | ## Support
136 | If you find any issues or would like to request something changed, please feel free to [post an issue on Github](https://github.com/rob2d/use-viewport-sizes/issues/new).
137 |
138 | Otherwise, if this was useful and you'd like to show your support, no donations necessary, but please consider [checking out the repo](https://github.com/rob2d/use-viewport-sizes) and giving it a star (⭐).
139 |
140 | ## License ##
141 |
142 | Open Source **[MIT license](http://opensource.org/licenses/mit-license.php)**
143 |
--------------------------------------------------------------------------------
/babel.config.testing.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "presets": [
3 | "@babel/preset-env",
4 | "@babel/preset-react"
5 | ],
6 | "plugins": [
7 | "@babel/plugin-transform-runtime"
8 | ]
9 | };
--------------------------------------------------------------------------------
/doc/use-viewport-sizes.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rob2d/use-viewport-sizes/8db6a5254c6618637c4ae4d11fc6b861b2406f25/doc/use-viewport-sizes.gif
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | // jest.config.js
2 | module.exports = {
3 | transform: {
4 | '\\.js$': ['babel-jest', { configFile: './babel.config.testing.js' }]
5 | },
6 | testEnvironment: "jsdom"
7 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "use-viewport-sizes",
3 | "version": "0.8.1",
4 | "description": "a tiny TS-compatible React hook which allows you to track visible window viewport size in your components w/ an optional debounce, throttle or custom memo function for updates for optimal rendering.",
5 | "main": "./build/index.js",
6 | "types": "./build/index.d.ts",
7 | "scripts": {
8 | "start": "webpack --watch --mode development",
9 | "build": "webpack --mode production",
10 | "build:babel": "babel src --out-dir build --extensions \".js\"",
11 | "dev": "webpack-dev-server --env testServer --mode development --open",
12 | "prepublishOnly": "npm run build",
13 | "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage"
14 | },
15 | "peerDependencies": {
16 | "react": "^16.8.6 || ^17.0.0 || ^18.0.0 || ^19.0.0"
17 | },
18 | "devDependencies": {
19 | "@babel/cli": "^7.27.0",
20 | "@babel/core": "^7.26.10",
21 | "@babel/plugin-transform-runtime": "^7.26.10",
22 | "@babel/preset-env": "^7.26.9",
23 | "@babel/preset-react": "^7.26.3",
24 | "@testing-library/jest-dom": "^6.6.3",
25 | "@testing-library/react": "^16.2.0",
26 | "babel-loader": "^10.0.0",
27 | "copy-webpack-plugin": "^13.0.0",
28 | "jest": "^29.7.0",
29 | "jest-environment-jsdom": "^29.7.0",
30 | "react": "^18.3.0",
31 | "typescript": "^5.8.2",
32 | "webpack": "^5.98.0",
33 | "webpack-cli": "^6.0.1"
34 | },
35 | "keywords": [
36 | "react",
37 | "hooks",
38 | "hook",
39 | "react-hooks",
40 | "window",
41 | "resize",
42 | "viewport",
43 | "viewport-sizes",
44 | "sizes",
45 | "dimensions",
46 | "typescript",
47 | "responsive"
48 | ],
49 | "author": "Robert Concepcion III",
50 | "repository": "github:rob2d/use-viewport-sizes",
51 | "license": "MIT",
52 | "files": [
53 | "build/",
54 | "README.md"
55 | ]
56 | }
57 |
--------------------------------------------------------------------------------
/src/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'use-viewport-sizes' {
2 | export type VPSizesHasher = ({ vpW, vpH }: { vpW: number, vpH: number}) => string;
3 | export type VPSizesOptions ={
4 | debounceTimeout?: number,
5 | hasher?: VPSizesHasher,
6 | dimension?: 'both'
7 | };
8 |
9 | export type VPSizeOptions ={
10 | debounceTimeout?: number,
11 | hasher?: VPSizesHasher,
12 | dimension?: 'w'|'h'
13 | };
14 |
15 | /**
16 | * Hook which observes viewport dimensions. Returns [width, height] of
17 | * current visible viewport of app, or the specific dimension
18 | */
19 | export default function (input: number | VPSizesOptions): [vpW: number, vpH: number, triggerResize: Function];
20 |
21 | /**
22 | * Hook which observes viewport dimensions. Returns [width, height] of
23 | * current visible viewport of app, or the specific dimension
24 | */
25 | export default function (input: VPSizeOptions): [dimensionValue: number, triggerResize: Function];
26 |
27 | /**
28 | * Hook which observes viewport dimensions. Returns [width, height] of
29 | * current visible viewport of app, or the specific dimension
30 | */
31 | export default function (input: VPSizesHasher): [vpW: number, vpH: number, triggerResize: Function];
32 |
33 | /**
34 | * Hook which observes viewport dimensions. Returns [width, height] of
35 | * current visible viewport of app, or the specific dimension
36 | */
37 | export default function (): [vpW: number, vpH: number, triggerResize: Function];
38 | }
39 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | useState,
3 | useMemo,
4 | useCallback,
5 | useRef,
6 | useLayoutEffect
7 | } from 'react';
8 |
9 | function getVpWidth() {
10 | return Math.max(
11 | globalThis?.document?.documentElement?.clientWidth || 0,
12 | globalThis?.innerWidth || 0
13 | );
14 | }
15 |
16 |
17 | function getVpHeight() {
18 | return Math.max(
19 | globalThis?.document?.documentElement?.clientHeight || 0,
20 | globalThis?.innerHeight || 0
21 | );
22 | }
23 |
24 | // Avoid useLayoutEffect warning during SSR
25 | // https://usehooks-ts.com/react-hook/use-isomorphic-layout-effect
26 | export const useIsomorphicLayoutEffect =
27 | typeof window !== 'undefined' ? useLayoutEffect : () => {};
28 |
29 | // =============== //
30 | // Shared State //
31 | // =============== //
32 |
33 | /**
34 | * listening component functions
35 | *
36 | * @type Function
37 | */
38 | const listeners = new Set();
39 |
40 | /**
41 | * contains a "hasher" which manages the behavior
42 | * of the listeners when applicable;
43 | * keys in the map are the direct listener reference
44 | * for minimum overhead and so we can reference
45 | * it easily on deletion
46 | *
47 | * @type Map
52 | */
53 | const resolverMap = new Map();
54 |
55 | let vpWidth = getVpWidth();
56 | let vpHeight = getVpHeight();
57 |
58 | /**
59 | * called to update resize dimensions with the handlers
60 | * passed themselves; separated from the actual
61 | * pub-sub caller (onResize) so we can individually
62 | * dispatch subscription events and avoid updating all
63 | * components at once
64 | *
65 | * @param {*} listener
66 | * @param {*} vpWidth
67 | * @param {*} vpHeight
68 | */
69 | function triggerResizeListener(listener, vpWidth, vpHeight) {
70 | const params = { vpW: vpWidth, vpH: vpHeight };
71 | let hash;
72 |
73 | const { options, prevHash=undefined } = resolverMap?.get(listener) || {};
74 |
75 | if(!options.hasher) {
76 | switch (options?.dimension) {
77 | case 'w':
78 | hash = vpWidth;
79 | break;
80 | case 'h':
81 | hash = vpHeight;
82 | break;
83 | default:
84 | hash = (vpWidth << 16) | vpHeight;
85 | break;
86 | }
87 | }
88 | else {
89 | hash = options.hasher(params);
90 | }
91 |
92 | if(hash != prevHash) {
93 | const state = { ...params, options, hash };
94 | resolverMap.set(listener, {
95 | options,
96 | prevHash: hash,
97 | prevState: state
98 | });
99 |
100 | listener(state);
101 | }
102 | }
103 |
104 | /**
105 | * called to update resize dimensions;
106 | * only called once throughout hooks so if
107 | * using SSR, may be expensive to trigger in all
108 | * components with effect on paint as described in
109 | * readme
110 | */
111 | function onResize() {
112 | vpWidth = getVpWidth();
113 | vpHeight = getVpHeight();
114 |
115 | listeners.forEach(listener =>
116 | triggerResizeListener(listener, vpWidth, vpHeight)
117 | );
118 | }
119 |
120 | // =============== //
121 | // the Hook //
122 | // =============== //
123 |
124 | export default function useViewportSizes(input) {
125 | const hasher = ((typeof input == 'function') ?
126 | input :
127 | input?.hasher
128 | ) || undefined;
129 |
130 | const debounceTimeout = ((typeof input?.debounceTimeout == 'number') ?
131 | input?.debounceTimeout : 0
132 | );
133 |
134 | const throttleTimeout = ((typeof input == 'number') ?
135 | input :
136 | input?.throttleTimeout
137 | ) || 0;
138 |
139 | const dimension = input?.dimension || 'both';
140 |
141 | const options = {
142 | ...(typeof input == 'object' ? input : {}),
143 | dimension,
144 | hasher
145 | };
146 |
147 | const [state, setState] = useState(() => {
148 | const defaultState = { vpW: vpWidth, vpH: vpHeight };
149 | return options.hasher ? options.hasher(defaultState) : defaultState;
150 | });
151 | const debounceTimeoutRef = useRef(undefined);
152 | const throttleTimeoutRef = useRef(undefined);
153 | const lastThrottledRef = useRef(undefined);
154 |
155 | const listener = useCallback(nextState => {
156 | if(!debounceTimeout && !throttleTimeout) {
157 | setState(nextState);
158 | return;
159 | }
160 |
161 | if(debounceTimeout) {
162 | if(debounceTimeoutRef.current) {
163 | clearTimeout(debounceTimeoutRef.current);
164 | }
165 |
166 | debounceTimeoutRef.current = setTimeout(() => (
167 | setState(nextState)
168 | ), debounceTimeout);
169 | }
170 |
171 | if(throttleTimeout) {
172 | const lastTick = lastThrottledRef.current;
173 | const timeSinceLast = (!lastTick ? throttleTimeout : Date.now() - lastTick);
174 |
175 | clearTimeout(throttleTimeoutRef);
176 |
177 | throttleTimeoutRef.current = setTimeout(() => {
178 | lastThrottledRef.current = new Date().getTime();
179 | const vpWidth = getVpWidth();
180 | const vpHeight = getVpHeight();
181 |
182 | const dimensionsUpdated = new Set();
183 |
184 | if(vpHeight != state.vpH) {
185 | dimensionsUpdated.add('h');
186 | }
187 |
188 | if(vpWidth != state.vpW) {
189 | dimensionsUpdated.add('w');
190 | }
191 |
192 | if(dimensionsUpdated.has('w') || dimensionsUpdated.has('h')) {
193 | dimensionsUpdated.add('both');
194 | }
195 |
196 | if(dimensionsUpdated.has(dimension)) {
197 | setState({ vpW: vpWidth, vpH: vpHeight });
198 | }
199 | }, Math.max(0, throttleTimeout - timeSinceLast));
200 | }
201 | }, [debounceTimeoutRef, hasher, dimension, state]);
202 |
203 | useIsomorphicLayoutEffect(() => {
204 | resolverMap.set(listener, {
205 | options,
206 | prevHash: state.hash || undefined
207 | });
208 |
209 | listeners.add(listener);
210 |
211 | // if first resize listener, add resize event
212 |
213 | if(window && listeners.size == 1) {
214 | window.addEventListener('resize', onResize);
215 | onResize();
216 | }
217 |
218 | // if additional resize listener, trigger it on the new listener
219 |
220 | else {
221 | triggerResizeListener(listener, vpWidth, vpHeight);
222 | }
223 |
224 | // clean up listeners on unmount
225 |
226 | return () => {
227 | resolverMap.delete(listener);
228 | listeners.delete(listener);
229 |
230 | if(listeners.size == 0) {
231 | window?.removeEventListener?.('resize', onResize);
232 | }
233 | };
234 | }, [listener]);
235 |
236 | let dimensionHash;
237 |
238 | switch (dimension) {
239 | default:
240 | case 'both': {
241 | dimensionHash = `${state.vpW}_${state.vpH}`;
242 | break;
243 | }
244 | case 'w': {
245 | dimensionHash = state.vpW || 0;
246 | break;
247 | }
248 | case 'h': {
249 | dimensionHash = state.vpH || 0;
250 | break;
251 | }
252 | }
253 |
254 | const returnValue = useMemo(() => {
255 | switch (dimension) {
256 | default: { return [state.vpW || 0, state.vpH || 0, onResize] }
257 | case 'w': { return [state.vpW || 0, onResize] }
258 | case 'h': { return [state.vpH || 0, onResize] }
259 | }
260 | }, [dimensionHash, onResize, dimension]);
261 |
262 | return returnValue;
263 | }
264 |
--------------------------------------------------------------------------------
/src/index.test.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import React from 'react';
3 | import { render, screen, act, fireEvent } from '@testing-library/react';
4 | import useViewportSizes from './index';
5 |
6 | function DimensionView({ options }) {
7 | const [dimension] = useViewportSizes(options);
8 | return (
9 |
12 | );
13 | }
14 |
15 | function DimensionsView({ options }) {
16 | const [vpW, vpH] = useViewportSizes(options);
17 | return (
18 |
19 |
{vpW}
20 |
{vpH}
21 |
22 | );
23 | }
24 |
25 | function setViewportDimensions(width, height) {
26 | act(() => {
27 | window.innerWidth = width;
28 | window.innerHeight = height;
29 | });
30 |
31 | fireEvent(window, new Event('resize'));
32 | }
33 |
34 | describe('useViewportSizes', () => {
35 | afterEach(() => {
36 | jest.useRealTimers();
37 | });
38 | describe('calling with no options', () => {
39 | test('renders viewport width/height when run with no arguments', async () => {
40 | setViewportDimensions(640, 480);
41 | render( );
42 | expect(screen.getByTestId('vpw').textContent).toEqual('640');
43 | expect(screen.getByTestId('vph').textContent).toEqual('480');
44 | });
45 |
46 | test('updates viewport width/height read when the window is resized', async () => {
47 | setViewportDimensions(640, 480);
48 | render( );
49 | expect(screen.getByTestId('vpw').textContent).toEqual('640');
50 | expect(screen.getByTestId('vph').textContent).toEqual('480');
51 |
52 | setViewportDimensions(50, 100);
53 | expect(screen.getByTestId('vpw').textContent).toEqual('50');
54 | expect(screen.getByTestId('vph').textContent).toEqual('100');
55 | });
56 | });
57 |
58 | describe('calling with one dimension option passed', () => {
59 | test('renders width of viewport when passed dimension: `w`, and updates this', async () => {
60 | setViewportDimensions(640, 480);
61 | render( );
62 | expect(screen.getByTestId('dimension').textContent).toEqual('640');
63 |
64 | setViewportDimensions(44, 80);
65 | expect(screen.getByTestId('dimension').textContent).toEqual('44');
66 | });
67 |
68 | test('renders width of viewport when passed dimension: `h`, and updates this', async () => {
69 | setViewportDimensions(640, 480);
70 | render( );
71 | expect(screen.getByTestId('dimension').textContent).toEqual('480');
72 |
73 | setViewportDimensions(44, 80);
74 | expect(screen.getByTestId('dimension').textContent).toEqual('80');
75 | });
76 | });
77 |
78 | describe('other scenarios', () => {
79 | test('works with a custom hasher to only update when a breakpoint changes', async () => {
80 | jest.useFakeTimers();
81 | setViewportDimensions(640, 480);
82 | const options = {
83 | hasher: ({ vpW }) => {
84 | if(vpW <= 240) { return 'xs' }
85 | if(vpW <= 320) { return 'sm' }
86 | else if(vpW <= 640) { return 'md' }
87 | else return 'lg';
88 | }
89 | };
90 |
91 | render( );
92 | expect(screen.getByTestId('vpw').textContent).toEqual('640');
93 |
94 | setViewportDimensions(639, 480);
95 | jest.runAllTimers();
96 | expect(screen.getByTestId('vpw').textContent).toEqual('640');
97 |
98 | setViewportDimensions(645, 480);
99 | jest.runAllTimers();
100 |
101 | expect(screen.getByTestId('vpw').textContent).toEqual('645');
102 |
103 | setViewportDimensions(240, 480);
104 | jest.runAllTimers();
105 | expect(screen.getByTestId('vpw').textContent).toEqual('240');
106 |
107 | setViewportDimensions(238, 480);
108 | jest.runAllTimers();
109 | expect(screen.getByTestId('vpw').textContent).toEqual('240');
110 |
111 | setViewportDimensions(300, 480);
112 | jest.runAllTimers();
113 | expect(screen.getByTestId('vpw').textContent).toEqual('300');
114 |
115 |
116 | setViewportDimensions(500, 200);
117 | jest.runAllTimers();
118 | expect(screen.getByTestId('vpw').textContent).toEqual('500');
119 | });
120 |
121 | test('debounces updated render of vpw/vph with debounceTimeout', async () => {
122 | jest.useFakeTimers();
123 | setViewportDimensions(640, 480);
124 | await jest.runAllTimersAsync();
125 | render( );
126 | await act(async () => {
127 | await jest.runAllTimersAsync();
128 | });
129 | expect(screen.getByTestId('vpw').textContent).toEqual('640');
130 | expect(screen.getByTestId('vph').textContent).toEqual('480');
131 |
132 | await act(async () => {
133 | await jest.advanceTimersByTimeAsync(100);
134 | });
135 | expect(screen.getByTestId('vpw').textContent).toEqual('640');
136 | expect(screen.getByTestId('vph').textContent).toEqual('480');
137 |
138 | await act(async () => {
139 | await jest.advanceTimersByTimeAsync(450);
140 | setViewportDimensions(100, 100);
141 | await jest.runAllTimersAsync();
142 | });
143 | expect(screen.getByTestId('vpw').textContent).toEqual('100');
144 | expect(screen.getByTestId('vph').textContent).toEqual('100');
145 | });
146 |
147 | test('throttles updated render of vpw/vph with throttleTimeout', async () => {
148 | jest.useFakeTimers();
149 |
150 | setViewportDimensions(640, 480);
151 | render( );
152 |
153 | await act(async () => {
154 | await jest.runAllTimersAsync();
155 | setViewportDimensions(200, 200);
156 | await jest.advanceTimersByTimeAsync(50);
157 | });
158 |
159 | expect(screen.getByTestId('vpw').textContent).toEqual('640');
160 | expect(screen.getByTestId('vph').textContent).toEqual('480');
161 |
162 | await act(async () => {
163 | await jest.advanceTimersByTimeAsync(150);
164 | });
165 |
166 | expect(screen.getByTestId('vpw').textContent).toEqual('200');
167 | expect(screen.getByTestId('vph').textContent).toEqual('200');
168 |
169 | await act(async () => {
170 | await jest.advanceTimersByTimeAsync(450);
171 | setViewportDimensions(100, 100);
172 | await jest.runAllTimersAsync();
173 | });
174 |
175 | expect(screen.getByTestId('vpw').textContent).toEqual('100');
176 | expect(screen.getByTestId('vph').textContent).toEqual('100');
177 | });
178 | });
179 | });
180 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const CopyPlugin = require('copy-webpack-plugin')
3 |
4 | module.exports = {
5 | entry: './src/index.js',
6 | output: {
7 | path: path.resolve(__dirname, 'build'),
8 | filename: 'index.js',
9 | libraryTarget: 'commonjs2'
10 | },
11 | module: {
12 | rules: [
13 | {
14 | test: /\.js$/,
15 | include: path.resolve(__dirname, 'src'),
16 | exclude: /(node_modules|bower_components|build)/,
17 | use: {
18 | loader: 'babel-loader',
19 | }
20 | }
21 | ]
22 | },
23 | plugins: [
24 | new CopyPlugin({
25 | patterns: [{
26 | from: path.join(__dirname, 'src', 'index.d.ts'),
27 | to: path.join(__dirname, 'build', 'index.d.ts')
28 | }]
29 | })
30 | ],
31 | externals: {
32 | 'react': 'commonjs react'
33 | }
34 | };
--------------------------------------------------------------------------------