diff --git a/jest.config.js b/jest.config.js
index 0440f8a..5f7e8da 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -5,5 +5,5 @@ module.exports = {
   // setupFilesAfterEnv: ['<rootDir>/test-utils/setupTests.js'],
   // added "(?<!types.)" as a negative lookbehind to the default pattern
   // to exclude .types.test.ts patterns fro being picked up by jest
-  testRegex: '(/__tests__/.*|(\\.|/)(?<!types.)(test|spec))\\.[jt]sx?$'
+  testRegex: '(/__tests__/.*|(\\.|/)(?<!types.)(test|spec))\\.[jt]sx?$',
 };
diff --git a/package-lock.json b/package-lock.json
index b9341e1..4835c11 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,10 @@
       "name": "@muban/hooks",
       "version": "1.0.0-alpha.3",
       "license": "MIT",
+      "dependencies": {
+        "@types/lodash-es": "^4.17.6",
+        "lodash-es": "^4.17.21"
+      },
       "devDependencies": {
         "@babel/core": "^7.12.10",
         "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1",
@@ -38,6 +42,7 @@
         "@types/jest": "^26.0.15",
         "babel-jest": "^26.6.3",
         "jest": "^26.3.0",
+        "jsdom-testing-mocks": "^1.2.2",
         "nanoid": "^3.1.30",
         "npm-run-all": "^4.1.5",
         "plop": "^3.0.6",
@@ -9357,14 +9362,12 @@
     "node_modules/@types/lodash": {
       "version": "4.14.178",
       "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz",
-      "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==",
-      "dev": true
+      "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw=="
     },
     "node_modules/@types/lodash-es": {
-      "version": "4.17.5",
-      "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.5.tgz",
-      "integrity": "sha512-SHBoI8/0aoMQWAgUHMQ599VM6ZiSKg8sh/0cFqqlQQMyY9uEplc0ULU5yQNzcvdR4ZKa0ey8+vFmahuRbOCT1A==",
-      "dev": true,
+      "version": "4.17.6",
+      "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.6.tgz",
+      "integrity": "sha512-R+zTeVUKDdfoRxpAryaQNRKk3105Rrgx2CFRClIgRGaqDTdjsm8h6IYA8ir584W3ePzkZfst5xIgDwYrlh9HLg==",
       "dependencies": {
         "@types/lodash": "*"
       }
@@ -13200,6 +13203,12 @@
         "node": ">=4.0.0"
       }
     },
+    "node_modules/css-mediaquery": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz",
+      "integrity": "sha1-aiw3NEkoYYYxxUvTPO3TAdoYvqA=",
+      "dev": true
+    },
     "node_modules/css-select": {
       "version": "4.1.3",
       "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz",
@@ -19876,6 +19885,21 @@
         }
       }
     },
+    "node_modules/jsdom-testing-mocks": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/jsdom-testing-mocks/-/jsdom-testing-mocks-1.2.2.tgz",
+      "integrity": "sha512-msu1L8K/bBf6QxJzmsWviyvoc0XS+FJKazSrdQu2xx4Kg+cePercoh+H0U4tdZMVtjOiyLSEIUMH3uwF8+omVg==",
+      "dev": true,
+      "dependencies": {
+        "css-mediaquery": "^0.1.2"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "peerDependencies": {
+        "react": ">=16"
+      }
+    },
     "node_modules/jsdom/node_modules/acorn": {
       "version": "8.6.0",
       "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz",
@@ -20215,8 +20239,7 @@
     "node_modules/lodash-es": {
       "version": "4.17.21",
       "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
-      "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
-      "dev": true
+      "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
     },
     "node_modules/lodash.debounce": {
       "version": "4.0.8",
@@ -35648,14 +35671,12 @@
     "@types/lodash": {
       "version": "4.14.178",
       "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz",
-      "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==",
-      "dev": true
+      "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw=="
     },
     "@types/lodash-es": {
-      "version": "4.17.5",
-      "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.5.tgz",
-      "integrity": "sha512-SHBoI8/0aoMQWAgUHMQ599VM6ZiSKg8sh/0cFqqlQQMyY9uEplc0ULU5yQNzcvdR4ZKa0ey8+vFmahuRbOCT1A==",
-      "dev": true,
+      "version": "4.17.6",
+      "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.6.tgz",
+      "integrity": "sha512-R+zTeVUKDdfoRxpAryaQNRKk3105Rrgx2CFRClIgRGaqDTdjsm8h6IYA8ir584W3ePzkZfst5xIgDwYrlh9HLg==",
       "requires": {
         "@types/lodash": "*"
       }
@@ -38804,6 +38825,12 @@
         }
       }
     },
+    "css-mediaquery": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz",
+      "integrity": "sha1-aiw3NEkoYYYxxUvTPO3TAdoYvqA=",
+      "dev": true
+    },
     "css-select": {
       "version": "4.1.3",
       "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz",
@@ -43934,6 +43961,15 @@
         }
       }
     },
+    "jsdom-testing-mocks": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/jsdom-testing-mocks/-/jsdom-testing-mocks-1.2.2.tgz",
+      "integrity": "sha512-msu1L8K/bBf6QxJzmsWviyvoc0XS+FJKazSrdQu2xx4Kg+cePercoh+H0U4tdZMVtjOiyLSEIUMH3uwF8+omVg==",
+      "dev": true,
+      "requires": {
+        "css-mediaquery": "^0.1.2"
+      }
+    },
     "jsesc": {
       "version": "2.5.2",
       "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
@@ -44174,8 +44210,7 @@
     "lodash-es": {
       "version": "4.17.21",
       "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
-      "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
-      "dev": true
+      "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
     },
     "lodash.debounce": {
       "version": "4.0.8",
diff --git a/package.json b/package.json
index 54e581c..c815e49 100644
--- a/package.json
+++ b/package.json
@@ -73,6 +73,7 @@
     "@types/jest": "^26.0.15",
     "babel-jest": "^26.6.3",
     "jest": "^26.3.0",
+    "jsdom-testing-mocks": "^1.2.2",
     "nanoid": "^3.1.30",
     "npm-run-all": "^4.1.5",
     "plop": "^3.0.6",
@@ -83,5 +84,9 @@
   },
   "engines": {
     "npm": ">= 7.0.0"
+  },
+  "dependencies": {
+    "@types/lodash-es": "^4.17.6",
+    "lodash-es": "^4.17.21"
   }
 }
diff --git a/src/index.ts b/src/index.ts
index 22e680e..a8e877f 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,5 @@
 /* PLOP_ADD_EXPORT */
+export * from './useResizeObserver/useResizeObserver';
 export * from './useToggle/useToggle';
 export * from './useEventListener/useEventListener';
 export * from './useKeyboardEvent/useKeyboardEvent';
diff --git a/src/useResizeObserver/useResizeObserver.stories.mdx b/src/useResizeObserver/useResizeObserver.stories.mdx
new file mode 100644
index 0000000..b9caa00
--- /dev/null
+++ b/src/useResizeObserver/useResizeObserver.stories.mdx
@@ -0,0 +1,60 @@
+import { Meta } from '@storybook/addon-docs';
+
+<Meta
+  title="useResizeObserver/docs"
+/>
+
+# useResizeObserver
+
+Please add a description about the `useResizeObserver` hook.
+
+## Reference
+
+```ts
+function useResizeObserver(
+  source: DomElementOrRef,
+  callback: KeyDownCallback,
+  debounceTime?: number,
+): void
+```
+
+### Parameters
+
+* `source` - The DOM Element or Ref that you want to observe for resizes.
+* `callback` - The function to invoke when the source is resized.
+* `debounceTime` - The amount of debounce you want to apply to the callback function.
+
+### Returns
+
+* `void`
+
+## Usage
+
+```ts
+useResizeObserver(refs.someRef, () => {
+  console.log('someRef was resized!')
+}, 100);
+````
+
+```ts
+const Demo = defineComponent({
+  name: 'demo',
+  refs: {
+    someRef: 'some-ref',
+  },
+  setup({ refs }) {
+
+    // The most basic implementation of watching an element for resizes.
+    useResizeObserver(refs.someRef, () => {
+        console.log('someRef was resized')
+    });
+
+    // The callback method can easily be debounced to reduce the amount of triggers.
+    useResizeObserver(refs.someRef, () => {
+        console.log('someRef was resized.')
+    }, 500);
+
+    return [];
+  }
+})
+```
diff --git a/src/useResizeObserver/useResizeObserver.stories.ts b/src/useResizeObserver/useResizeObserver.stories.ts
new file mode 100644
index 0000000..17d2455
--- /dev/null
+++ b/src/useResizeObserver/useResizeObserver.stories.ts
@@ -0,0 +1,68 @@
+/* eslint-disable import/no-extraneous-dependencies */
+import { bind, computed, defineComponent, ref } from '@muban/muban';
+import type { Story } from '@muban/storybook/types-6-0';
+import { html } from '@muban/template';
+import { useResizeObserver } from './useResizeObserver';
+import { useStorybookLog } from '../hooks/useStorybookLog';
+
+export default {
+  title: 'useResizeObserver',
+};
+
+export const Demo: Story = () => ({
+  component: defineComponent({
+    name: 'story',
+    refs: {
+      testArea: 'test-area',
+      label: 'label',
+      resizeButton: 'resize-button',
+    },
+    setup({ refs }) {
+      const [logBinding, log] = useStorybookLog(refs.label);
+      const width = ref(100);
+
+      function onResizeObserverUpdate(): void {
+        log('resize observer triggered');
+      }
+
+      useResizeObserver(refs.testArea, onResizeObserverUpdate, 100);
+
+      return [
+        logBinding,
+        bind(refs.testArea, {
+          style: {
+            width: computed(() => `${width.value}%`),
+          },
+        }),
+        bind(refs.resizeButton, {
+          click() {
+            const newWidth = width.value - 25;
+            width.value = newWidth > 0 ? newWidth : 100;
+          },
+        }),
+      ];
+    },
+  }),
+  template: () => html`<div data-component="story">
+    <div class="alert alert-primary">
+      <h4 class="alert-heading">Instructions!</h4>
+      <p>Resize the window or click on the resize button to see the events being triggered.</p>
+      <p class="mb-0">
+        Note: The <code>callback</code> is debounced by <u>100ms</u> to avoid it from being called
+        way too much for the sake of the demo.
+      </p>
+    </div>
+    <div data-ref="label" />
+    <div class="card border-dark" data-ref="test-area">
+      <div class="card-header">Test Area</div>
+      <div class="card-body">
+        <p>
+          The resize observer is attached to this element, so it will trigger the callback method if
+          it's resized.
+        </p>
+        <button type="button" data-ref="resize-button" class="btn btn-success">Resize</button>
+      </div>
+    </div>
+  </div>`,
+});
+Demo.storyName = 'demo';
diff --git a/src/useResizeObserver/useResizeObserver.test.ts b/src/useResizeObserver/useResizeObserver.test.ts
new file mode 100644
index 0000000..0ee4789
--- /dev/null
+++ b/src/useResizeObserver/useResizeObserver.test.ts
@@ -0,0 +1,52 @@
+import { createMockElementRef, runComponentSetup } from '@muban/test-utils';
+import { mockResizeObserver } from 'jsdom-testing-mocks';
+import { useResizeObserver } from './useResizeObserver';
+import { timeout } from './useResizeObserver.test.utils';
+
+jest.mock('@muban/muban', () => jest.requireActual('@muban/test-utils').getMubanLifecycleMock());
+
+const resizeObserver = mockResizeObserver();
+
+describe('useResizeObserver', () => {
+  it('should not crash', () => {
+    const { ref } = createMockElementRef();
+
+    runComponentSetup(() => {
+      useResizeObserver(ref, () => undefined);
+    });
+  });
+
+  it('should attach a resize observer to a ref', async () => {
+    const mockHandler = jest.fn();
+    const { ref, target } = createMockElementRef();
+
+    await runComponentSetup(
+      () => {
+        useResizeObserver(ref, mockHandler);
+      },
+      async () => {
+        resizeObserver.resize(target);
+        await timeout(0);
+      },
+    );
+
+    expect(mockHandler).toBeCalledTimes(1);
+  });
+
+  it('should attach a resize observer to a HTML element', async () => {
+    const mockHandler = jest.fn();
+    const { target } = createMockElementRef();
+
+    await runComponentSetup(
+      () => {
+        useResizeObserver(target, mockHandler);
+      },
+      async () => {
+        resizeObserver.resize(target);
+        await timeout(0);
+      },
+    );
+
+    expect(mockHandler).toBeCalledTimes(1);
+  });
+});
diff --git a/src/useResizeObserver/useResizeObserver.test.utils.ts b/src/useResizeObserver/useResizeObserver.test.utils.ts
new file mode 100644
index 0000000..2aa653f
--- /dev/null
+++ b/src/useResizeObserver/useResizeObserver.test.utils.ts
@@ -0,0 +1,17 @@
+/**
+ * A helper method that allows you to easily create a timeout with the async/await notation
+ *
+ * ```ts
+ *   async function someFunction(){
+ *     await timeout(100);
+ *     console.log('This is delayed by 100ms');
+ *   }
+ * ```
+ *
+ * @param duration The duration that you want to create the timeout for.
+ */
+export async function timeout(duration: number): Promise<void> {
+  return new Promise((resolve) => {
+    setTimeout(resolve, duration);
+  });
+}
diff --git a/src/useResizeObserver/useResizeObserver.ts b/src/useResizeObserver/useResizeObserver.ts
new file mode 100644
index 0000000..3474d7a
--- /dev/null
+++ b/src/useResizeObserver/useResizeObserver.ts
@@ -0,0 +1,88 @@
+import { onMounted, onUnmounted, watchEffect } from '@muban/muban';
+import { debounce } from 'lodash-es';
+import type { DomElementOrRef } from '../utils/util.types';
+import { getElement } from '../utils/getElement';
+
+const resizeObserverInstanceCallbacks = new Map<Element, ResizeObserverCallback>();
+let resizeObserverInstance: ResizeObserver;
+
+/**
+ * We create only one ResizeObserver instance, and we create it on demand. This way we keep the
+ * amount of observers to a minimum.
+ */
+function getResizeObserverInstance(): ResizeObserver {
+  if (!resizeObserverInstance) {
+    resizeObserverInstance = new ResizeObserver((entries, observer) => {
+      entries.forEach((entry) => {
+        resizeObserverInstanceCallbacks.get(entry.target)?.([entry], observer);
+      });
+    });
+  }
+  return resizeObserverInstance;
+}
+
+/**
+ * This helper method makes it easy to add a `DomElementOrRef` to the `resizeObserverInstance`.
+ *
+ * @param source The source that you want to observe for resizes.
+ * @param callback The function to invoke when the element is resized.
+ */
+function addToResizeObserver(source: DomElementOrRef, callback: ResizeObserverCallback): void {
+  const element = getElement(source);
+  const resizeObserver = getResizeObserverInstance();
+
+  if (element) {
+    resizeObserverInstanceCallbacks.set(element, callback);
+    resizeObserver.observe(element);
+  }
+}
+
+/**
+ * This helper method makes it easy to remove a `DomElementOrRef` from the `resizeObserverInstance`.
+ *
+ * @param source The source that you want to unobserve for resizes.
+ */
+function removeFromResizeObserver(source: DomElementOrRef): void {
+  const element = getElement(source);
+  const resizeObserver = getResizeObserverInstance();
+
+  if (element) {
+    resizeObserver.unobserve(element);
+    resizeObserverInstanceCallbacks.delete(element);
+  }
+}
+
+/**
+ * A wrapper around the native ResizeObserver that automatically cleans up when unmounted. It reuses
+ * the same instance when applied on a specific element to reduce the amount of instances created.
+ *
+ * @param source The DOM Element or Ref that you want to observe for resizes.
+ * @param callback The function to invoke when the element is resized.
+ * @param debounceTime The mount of debounce you want to apply to the callback function.
+ */
+export const useResizeObserver = (
+  source: DomElementOrRef,
+  callback: ResizeObserverCallback,
+  debounceTime?: number,
+): void => {
+  const debouncedCallback = debounce(callback, debounceTime);
+
+  if (source instanceof HTMLElement || source instanceof SVGElement) {
+    onMounted(() => {
+      addToResizeObserver(source, debouncedCallback);
+    });
+
+    onUnmounted(() => {
+      removeFromResizeObserver(source);
+    });
+  } else {
+    const removeWatchEffect = watchEffect(() => {
+      removeFromResizeObserver(source);
+      addToResizeObserver(source, debouncedCallback);
+    });
+
+    onUnmounted(() => {
+      removeWatchEffect();
+    });
+  }
+};
diff --git a/src/useResizeObserver/useResizeObserver.utils.ts b/src/useResizeObserver/useResizeObserver.utils.ts
new file mode 100644
index 0000000..6744e59
--- /dev/null
+++ b/src/useResizeObserver/useResizeObserver.utils.ts
@@ -0,0 +1 @@
+export const { ResizeObserver } = window;
diff --git a/src/useResizeObserver/useResizeObserverStories.test.ts b/src/useResizeObserver/useResizeObserverStories.test.ts
new file mode 100644
index 0000000..ed31bc8
--- /dev/null
+++ b/src/useResizeObserver/useResizeObserverStories.test.ts
@@ -0,0 +1,23 @@
+import '@testing-library/jest-dom';
+import { render, waitFor } from '@muban/testing-library';
+import { mockResizeObserver } from 'jsdom-testing-mocks';
+import { Demo } from './useResizeObserver.stories';
+
+const resizeObserver = mockResizeObserver();
+
+describe('useResizeObserver stories', () => {
+  it('should render', () => {
+    const { getByText } = render(Demo);
+
+    expect(getByText('Test Area')).toBeInTheDocument();
+  });
+
+  it('should trigger a resize', async () => {
+    const { getByRef, getByText } = render(Demo);
+    const target = getByRef('test-area');
+
+    resizeObserver.resize(target);
+
+    await waitFor(() => expect(getByText('resize observer triggered')).toBeInTheDocument());
+  });
+});