Repository: streamich/react-use
Pull request #2608 (2024-12-02 15:09:43)

feat: add onChange callback to useWindowSize (#915)
# Description

This update adds an `onChange` callback to the `useWindowSize` hook, allowing custom logic to be executed on window resize. It also integrates `@storybook/addon-actions` to log resize events in Storybook's "Actions" panel.

## Type of change

- [ ] Bug fix _(non-breaking change which fixes an issue)_
- [X] New feature _(non-breaking change which adds functionality)_
- [ ] **Breaking change** _(fix or feature that would cause existing functionality to not work as before)_

# Checklist

- [X] Read the [Contributing Guide](https://github.com/streamich/react-use/blob/master/CONTRIBUTING.md)
- [X] Perform a code self-review
- [X] Comment the code, particularly in hard-to-understand areas
- [X] Add documentation
- [X] Add hook's story at Storybook
- [X] Cover changes with tests
- [X] Ensure the test suite passes (`yarn test`)
- [X] Provide 100% tests coverage
- [X] Make sure code lints (`yarn lint`). Fix it with `yarn lint:fix` in case of failure.
- [X] Make sure types are fine (`yarn lint:types`).



Patch:
diff --git a/docs/useWindowSize.md b/docs/useWindowSize.md
index e84501cf..836d8c10 100644
--- a/docs/useWindowSize.md
+++ b/docs/useWindowSize.md
@@ -19,3 +19,18 @@ const Demo = () => {
   );
 };
 ```
+
+## Reference
+
+```js
+useWindowSize(options);
+```
+
+- `initialWidth` — Initial width value for non-browser environments.
+- `initialHeight` — Initial height value for non-browser environments.
+- `onChange` — Callback function triggered when the window size changes.
+
+## Related hooks
+
+- [useSize](./useSize.md)
+- [useMeasure](./useMeasure.md)
\ No newline at end of file
diff --git a/src/useWindowSize.ts b/src/useWindowSize.ts
index 3137307e..10d94027 100644
--- a/src/useWindowSize.ts
+++ b/src/useWindowSize.ts
@@ -3,29 +3,52 @@ import { useEffect } from 'react';
 import useRafState from './useRafState';
 import { isBrowser, off, on } from './misc/util';
 
-const useWindowSize = (initialWidth = Infinity, initialHeight = Infinity) => {
+// Define the type for options that can be passed to the hook
+interface Options {
+  initialWidth?: number; // Initial width of the window (Default value is Infinity)
+  initialHeight?: number; // Initial height of the window (Default value is Infinity)
+  onChange?: (width: number, height: number) => void; // Callback function to execute on window resize (optional)
+}
+
+const useWindowSize = ({
+  initialWidth = Infinity,
+  initialHeight = Infinity,
+  onChange,
+}: Options = {}) => {
+  // Use the useRafState hook to maintain the current window size (width and height)
   const [state, setState] = useRafState<{ width: number; height: number }>({
     width: isBrowser ? window.innerWidth : initialWidth,
     height: isBrowser ? window.innerHeight : initialHeight,
   });
 
   useEffect((): (() => void) | void => {
+    // Only run the effect on the browser (to avoid issues with SSR)
     if (isBrowser) {
       const handler = () => {
+        const width = window.innerWidth;
+        const height = window.innerHeight;
+
+        // Update the state with the new window size
         setState({
-          width: window.innerWidth,
-          height: window.innerHeight,
+          width,
+          height,
         });
+
+        // If an onChange callback is provided, call it with the new dimensions
+        if (onChange) onChange(width, height);
       };
 
+      // Add event listener for the resize event
       on(window, 'resize', handler);
 
+      // Cleanup function to remove the event listener when the component is unmounted (it's for performance optimization)
       return () => {
         off(window, 'resize', handler);
       };
     }
   }, []);
 
+  // Return the current window size (width and height)
   return state;
 };
 
diff --git a/stories/useWindowSize.story.tsx b/stories/useWindowSize.story.tsx
index 663b483a..3265057d 100644
--- a/stories/useWindowSize.story.tsx
+++ b/stories/useWindowSize.story.tsx
@@ -1,10 +1,14 @@
 import { storiesOf } from '@storybook/react';
 import * as React from 'react';
 import { useWindowSize } from '../src';
+import { action } from '@storybook/addon-actions'; // Import addon-actions
 import ShowDocs from './util/ShowDocs';
 
 const Demo = () => {
-  const { width, height } = useWindowSize();
+  const { width, height } = useWindowSize({
+    // Log the resize event to the Storybook actions panel
+    onChange: action('window resize'),
+  });
 
   return (
     <div>


Test patch:
diff --git a/tests/useWindowSize.test.tsx b/tests/useWindowSize.test.tsx
index dc242443..da5646f1 100644
--- a/tests/useWindowSize.test.tsx
+++ b/tests/useWindowSize.test.tsx
@@ -21,8 +21,8 @@ describe('useWindowSize', () => {
     expect(useWindowSize).toBeDefined();
   });
 
-  function getHook(...args) {
-    return renderHook(() => useWindowSize(...args));
+  function getHook(options?: any) {
+    return renderHook(() => useWindowSize(options));
   }
 
   function triggerResize(dimension: 'width' | 'height', value: number) {
@@ -44,7 +44,7 @@ describe('useWindowSize', () => {
   });
 
   it('should use passed parameters as initial values in case of non-browser use', () => {
-    const hook = getHook(1, 1);
+    const hook = getHook({ initialWidth: 1, initialHeight: 1 });
 
     expect(hook.result.current.height).toBe(isBrowser ? window.innerHeight : 1);
     expect(hook.result.current.width).toBe(isBrowser ? window.innerWidth : 1);
@@ -85,4 +85,27 @@ describe('useWindowSize', () => {
 
     expect(hook.result.current.width).toBe(2048);
   });
+
+  it('should call onChange callback on window resize', () => {
+    const onChange = jest.fn();
+    getHook({ onChange });
+
+    act(() => {
+      triggerResize('width', 720);
+      triggerResize('height', 480);
+      requestAnimationFrame.step();
+    });
+
+    expect(onChange).toHaveBeenCalledWith(720, 480);
+    expect(onChange).toHaveBeenCalledTimes(2);
+
+    act(() => {
+      triggerResize('width', 1920);
+      triggerResize('height', 1080);
+      requestAnimationFrame.step();
+    });
+
+    expect(onChange).toHaveBeenCalledWith(1920, 1080);
+    expect(onChange).toHaveBeenCalledTimes(4);
+  });
 });