├── .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 | [![npm](https://img.shields.io/npm/v/use-viewport-sizes.svg?color=blue)](https://www.npmjs.com/package/use-viewport-sizes) [![npm](https://img.shields.io/npm/dw/use-viewport-sizes.svg?color=red)]() [![GitHub issues](https://img.shields.io/github/issues-raw/rob2d/use-viewport-sizes.svg)](https://github.com/rob2d/use-viewport-sizes/issues) 4 | ![Github Workflow Status](https://github.com/rob2d/use-viewport-sizes/actions/workflows/node.js.yml/badge.svg) 5 | [![NPM](https://img.shields.io/npm/l/use-viewport-sizes.svg)](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 |
10 |
{dimension}
11 |
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 | }; --------------------------------------------------------------------------------