, Action> {
34 | // Use React hook to manage state
35 | const [response, setResponse] = useState>({ loading: false, data: null });
36 |
37 | const wrapRequset = useCallback(
38 | async (...args: P) => {
39 | try {
40 | setResponse(createResponse(true, response.data)); // Set loading state to true
41 | const data = await request(...args); // Execute request
42 | setResponse(createResponse(false, data)); // Set loading state to false and store the response data
43 | return data;
44 | } catch (e) {
45 | setResponse(createResponse(false, response.data));
46 | return response.data;
47 | }
48 | },
49 | [request, response]
50 | )
51 |
52 | return [response, { wrapRequset }]
53 | }
54 |
55 | export {
56 | useLoading
57 | }
58 |
--------------------------------------------------------------------------------
/src/useMeasure/__tests__/index.spec.tsx:
--------------------------------------------------------------------------------
1 | import { render, renderHook, screen } from '@testing-library/react';
2 | import { useMeasure, Measure } from '../';
3 |
4 | function mockResizeObserver(contentRect: Measure) {
5 | window.ResizeObserver =
6 | function (callback: (entries: { contentRect: Measure }[]) => void) {
7 | callback([{ contentRect }]);
8 | return {
9 | observe: jest.fn(),
10 | disconnect: jest.fn(),
11 | unobserve: jest.fn()
12 | }
13 | } as any;
14 | }
15 |
16 | describe('useMeasure', () => {
17 | it('should return correct initial value', () => {
18 | const noElement = { width: undefined, height: undefined, top: undefined, right: undefined, bottom: undefined, left: undefined, x: undefined, y: undefined };
19 |
20 | mockResizeObserver(noElement);
21 |
22 | const { result } = renderHook(() => useMeasure('#test'));
23 | expect(result.current[0]).toEqual({
24 | width: undefined,
25 | height: undefined,
26 | top: undefined,
27 | right: undefined,
28 | bottom: undefined,
29 | left: undefined,
30 | x: undefined,
31 | y: undefined
32 | });
33 | });
34 |
35 | it('should return correct measure value after resize', () => {
36 | mockResizeObserver({ width: 100, height: 200, top: 300, right: 400, bottom: 500, left: 600, x: 10, y: 20 });
37 |
38 | render(
);
39 | const { result } = renderHook(() => useMeasure('.test'));
40 |
41 | const measure = result.current[0];
42 | expect(measure.width).toEqual(100);
43 | expect(measure.height).toEqual(200);
44 | expect(measure.top).toEqual(300);
45 | expect(measure.right).toEqual(400);
46 | expect(measure.bottom).toEqual(500);
47 | expect(measure.left).toEqual(600);
48 | expect(measure.x).toEqual(10);
49 | expect(measure.y).toEqual(20);
50 | });
51 |
52 | it('should return correct measure value after resize with ref', () => {
53 | mockResizeObserver({ width: 100, height: 200, top: 300, right: 400, bottom: 500, left: 600, x: 10, y: 20 });
54 |
55 | render(useRef
);
56 | const ref = { current: screen.getAllByText('useRef')[0] };
57 | const { result } = renderHook(() => useMeasure(ref));
58 |
59 | const measure = result.current[0];
60 | expect(measure.width).toEqual(100);
61 | expect(measure.height).toEqual(200);
62 | expect(measure.top).toEqual(300);
63 | expect(measure.right).toEqual(400);
64 | expect(measure.bottom).toEqual(500);
65 | expect(measure.left).toEqual(600);
66 | expect(measure.x).toEqual(10);
67 | expect(measure.y).toEqual(20);
68 | });
69 | });
--------------------------------------------------------------------------------
/src/useMeasure/demo/index.tsx:
--------------------------------------------------------------------------------
1 | import { useRef } from 'react';
2 | import { useMeasure } from '../index';
3 |
4 | function Demo() {
5 | const ref = useRef(null);
6 | const [size] = useMeasure(ref);
7 |
8 | return (
9 |
10 | {size.width},{size.height},{size.top},{size.right},{size.bottom},{size.left},{size.x},{size.y}
11 |
12 |
13 | )
14 | }
15 |
16 | export default Demo;
--------------------------------------------------------------------------------
/src/useMeasure/index.tsx:
--------------------------------------------------------------------------------
1 | import { isType } from '../define';
2 | import { useState, useEffect, useCallback, RefObject } from 'react';
3 | import { ReturnValue } from '../define';
4 |
5 | // Define type for measurements of an element
6 | export type Measure = {
7 | width?: number;
8 | height?: number;
9 | top?: number,
10 | right?: number,
11 | bottom?: number,
12 | left?: number,
13 | x?: number,
14 | y?: number
15 | };
16 |
17 | // Define the types of arguments that can be passed to the hook
18 | type ElementArg = string | RefObject;
19 |
20 | // Define an interface for Actions, which are not used in this hook
21 | interface Action { };
22 |
23 | const useMeasure = (element: ElementArg): ReturnValue => {
24 | // Initialize state with the Measure object
25 | const [measure, setMeasure] = useState({
26 | width: undefined,
27 | height: undefined,
28 | top: undefined,
29 | right: undefined,
30 | bottom: undefined,
31 | left: undefined,
32 | x: undefined,
33 | y: undefined
34 | });
35 |
36 | // Define a callback function to get the element
37 | const getElement = useCallback(() => {
38 | if (isType(element, () => typeof element === 'string')) {
39 | // If the element argument is a string, get the element using the querySelector method
40 | return document.querySelector(element);
41 | } else {
42 | // If the element argument is a ref, get the element using the current property
43 | return element.current;
44 | }
45 | }, [element])
46 |
47 | // Attach a ResizeObserver to the element and update state with the new measurements
48 | useEffect(() => {
49 | const target = getElement();
50 |
51 | const resizeObserver = new ResizeObserver(([entry]) => {
52 | setMeasure({
53 | width: entry.contentRect.width,
54 | height: entry.contentRect.height,
55 | top: entry.contentRect.top,
56 | right: entry.contentRect.right,
57 | bottom: entry.contentRect.bottom,
58 | left: entry.contentRect.left,
59 | x: entry.contentRect.x,
60 | y: entry.contentRect.y
61 | });
62 | });
63 |
64 | if (target) {
65 | resizeObserver.observe(target);
66 | }
67 |
68 | // Detach the ResizeObserver when the component is unmounted
69 | return () => {
70 | resizeObserver.disconnect();
71 | };
72 | }, [element]);
73 |
74 | // Return the current measurements and an empty object, since no actions are performed in this hook
75 | return [measure, {}];
76 | };
77 |
78 | export { useMeasure };
--------------------------------------------------------------------------------
/src/useOutClick/__tests__/index.spec.tsx:
--------------------------------------------------------------------------------
1 | import { render, renderHook, fireEvent } from '@testing-library/react';
2 | import { useOutClick } from '../';
3 |
4 | describe('Test useOutClick', () => {
5 | it('handler is called when clicking outside of the element', () => {
6 | const handler = jest.fn();
7 | const TestComponent = () => {
8 | const [ref] = useOutClick(handler);
9 | return (
10 |
11 |
Inside element
12 | Outside element
13 |
14 | );
15 | };
16 |
17 | const { getByText } = render( );
18 | fireEvent.mouseDown(getByText('Outside element'));
19 |
20 | expect(handler).toHaveBeenCalledTimes(1);
21 | });
22 |
23 | it('handler is not called when clicking inside of the element', () => {
24 | const handler = jest.fn();
25 | const TestComponent = () => {
26 | const [ref] = useOutClick(handler);
27 | return (
28 |
29 |
Inside element
30 | Outside element
31 |
32 | );
33 | };
34 |
35 | const { getByText } = render( );
36 | fireEvent.mouseDown(getByText('Inside element'));
37 |
38 | expect(handler).not.toHaveBeenCalled();
39 | });
40 | });
--------------------------------------------------------------------------------
/src/useOutClick/demo/index.tsx:
--------------------------------------------------------------------------------
1 | import { useOutClick } from '../';
2 | function Demo() {
3 | const [ref] = useOutClick(() => {
4 | alert('click outter');
5 | });
6 | return
10 | }
11 |
12 | export default Demo;
--------------------------------------------------------------------------------
/src/useOutClick/index.tsx:
--------------------------------------------------------------------------------
1 | import { useRef, RefObject, useEffect } from 'react';
2 | import { ReturnValue } from '../define';
3 |
4 | interface Action { }
5 |
6 | function useOutClick(handler: () => void): ReturnValue, Action> {
7 | const ref = useRef(null);
8 |
9 | useEffect(() => {
10 | const handleClickOutside = (e: MouseEvent) => {
11 | if (ref.current && !ref.current.contains(e.target as HTMLElement)) {
12 | handler();
13 | }
14 | }
15 | window.addEventListener('mousedown', handleClickOutside);
16 |
17 |
18 | return () => {
19 | window.removeEventListener('mousedown', handleClickOutside);
20 | }
21 | }, [handler]);
22 |
23 | return [ref, {}];
24 | }
25 |
26 | export {
27 | useOutClick
28 | }
--------------------------------------------------------------------------------
/src/usePortal/__tests__/index.spec.tsx:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import { render, renderHook, screen } from '@testing-library/react';
3 | import { usePortal } from '../index';
4 | import { act } from 'react-dom/test-utils';
5 |
6 | describe('Test usePortal', () => {
7 | it('Does not render content if container does not exist', () => {
8 | const fn = jest.fn();
9 | fn.mockReturnValue(global component
);
10 |
11 | const { result } = renderHook((props) => usePortal(props.callback, props.dom), {
12 | initialProps: {
13 | callback: fn,
14 | dom: null
15 | }
16 | });
17 |
18 | act(() => {
19 | const content = result.current[1].render();
20 | content && render(content);
21 | });
22 |
23 | expect(document.body.children).toHaveLength(1);
24 | });
25 |
26 | it('Renders global component to body instead of root', () => {
27 | const fn = jest.fn();
28 | fn.mockReturnValue(global component
);
29 |
30 | const { result } = renderHook((props) => usePortal(props.callback, props.dom), {
31 | initialProps: {
32 | callback: fn,
33 | dom: document.body
34 | }
35 | });
36 |
37 | act(() => {
38 | const content = result.current[1].render();
39 | content && render(content);
40 | });
41 |
42 | const com = screen.getByText('global component');
43 | expect(com.parentElement).toEqual(document.body);
44 | expect(fn).toHaveBeenCalledTimes(1);
45 | });
46 | });
--------------------------------------------------------------------------------
/src/usePortal/demo/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback } from 'react';
2 | import { usePortal } from '../index';
3 | /**
4 | * @input
5 | * import { usePortal } from 'microhook';
6 | */
7 |
8 | function Node(props: { visible: boolean }) {
9 | const { visible } = props;
10 | console.log('rerender')
11 | return
12 | show: {String(visible)}
13 |
14 | }
15 | const MemoNode = React.memo(Node);
16 |
17 | function Demo() {
18 | const [visible, setVisible] = useState(false);
19 | const [loading, setLoading] = useState(false);
20 |
21 | /** To avoid re-render, please use useCallback and React.memo */
22 | const createExample = useCallback(() => , [visible]);
23 | const [, { render }] = usePortal(
24 | createExample,
25 | document.body
26 | );
27 | return (
28 |
29 | {String(visible)}\{String(loading)}
30 | {render() || null}
31 |
32 | { setVisible(!visible) }}>
33 | change visible
34 |
35 | { setLoading(!loading) }}>
36 | change loading
37 |
38 |
39 |
40 | )
41 | }
42 |
43 | export {
44 | Demo
45 | }
--------------------------------------------------------------------------------
/src/usePortal/index.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useMemo } from 'react';
2 | import { createPortal } from 'react-dom';
3 | import { ReturnValue } from '../define';
4 |
5 | interface Action {
6 | render: () => React.ReactPortal | null;
7 | }
8 |
9 | /**
10 | * @param callback Make sure to using with useCallback and React.memo, see `demo` for details.
11 | * @param container html element, like `document.body`.
12 | * @returns Render function: () => JSX.Element | null
13 | */
14 | function usePortal(callback: () => React.ReactNode, container: HTMLElement | null): ReturnValue {
15 | const content = useMemo(() => callback(), [callback]);
16 |
17 | const render = useCallback(
18 | () => {
19 | if (!container) return null;
20 | const portal = createPortal(content, container);
21 | return portal;
22 | },
23 | [content, container]
24 | );
25 |
26 | return [, { render }];
27 | }
28 |
29 | export {
30 | usePortal
31 | };
--------------------------------------------------------------------------------
/src/usePrefetch/__tests__/createResource.spec.ts:
--------------------------------------------------------------------------------
1 | import { createResource, isHTMLLinkElement } from '../createResource';
2 |
3 | describe('createResource', () => {
4 | test('should create an image resource with the correct url', () => {
5 | const url = 'https://example.com/image.jpg';
6 | const resource = createResource(url, 'image') as HTMLImageElement;
7 | expect(resource.nodeName).toBe('IMG');
8 | expect(resource.src).toBe(url);
9 | });
10 |
11 | test('should create an image resource with crossOrigin if provided', () => {
12 | const url = 'https://example.com/image.jpg';
13 | const crossOrigin = 'anonymous';
14 | const resource = createResource(url, 'image', crossOrigin) as HTMLImageElement;
15 | expect(resource.crossOrigin).toBe(crossOrigin);
16 | });
17 |
18 | test('should create a script resource with the correct url', () => {
19 | const url = 'https://example.com/script.js';
20 | const resource = createResource(url, 'script') as HTMLScriptElement;
21 | expect(resource.nodeName).toBe('SCRIPT');
22 | expect(resource.src).toBe(url);
23 | });
24 |
25 | test('should create a script resource with crossOrigin if provided', () => {
26 | const url = 'https://example.com/script.js';
27 | const crossOrigin = 'anonymous';
28 | const resource = createResource(url, 'script', crossOrigin) as HTMLScriptElement;
29 | expect(resource.crossOrigin).toBe(crossOrigin);
30 | });
31 |
32 | test('should create a link resource with the correct url', () => {
33 | const url = 'https://example.com/style.css';
34 | const resource = createResource(url, 'link') as HTMLLinkElement;
35 | expect(resource.nodeName).toBe('LINK');
36 | expect(resource.href).toBe(url);
37 | expect(resource.rel).toBe('stylesheet');
38 | });
39 |
40 | test('should create a link resource with crossOrigin if provided', () => {
41 | const url = 'https://example.com/style.css';
42 | const crossOrigin = 'use-credentials';
43 | const resource = createResource(url, 'link', crossOrigin) as HTMLLinkElement;
44 | expect(resource.crossOrigin).toBe(crossOrigin);
45 | });
46 |
47 | test('should create an audio resource with the correct url', () => {
48 | const url = 'https://example.com/audio.mp3';
49 | const resource = createResource(url, 'audio') as HTMLAudioElement;
50 | expect(resource.nodeName).toBe('AUDIO');
51 | expect(resource.src).toBe(url);
52 | });
53 |
54 | test('should create an audio resource with crossOrigin if provided', () => {
55 | const url = 'https://example.com/audio.mp3';
56 | const crossOrigin = 'anonymous';
57 | const resource = createResource(url, 'audio', crossOrigin) as HTMLAudioElement;
58 | expect(resource.crossOrigin).toBe(crossOrigin);
59 | });
60 |
61 | test('should create a video resource with the correct url', () => {
62 | const url = 'https://example.com/video.mp4';
63 | const resource = createResource(url, 'video') as HTMLVideoElement;
64 | expect(resource.nodeName).toBe('VIDEO');
65 | expect(resource.src).toBe(url);
66 | });
67 |
68 | test('should create a video resource with crossOrigin if provided', () => {
69 | const url = 'https://example.com/video.mp4';
70 | const crossOrigin = 'use-credentials';
71 | const resource = createResource(url, 'video', crossOrigin) as HTMLVideoElement;
72 | expect(resource.crossOrigin).toBe(crossOrigin);
73 | });
74 |
75 | test('should throw an error for an invalid resource type', () => {
76 | const url = 'https://example.com/file';
77 | expect(() => createResource(url, 'invalid' as any)).toThrow('Invalid resource type');
78 | });
79 | });
80 |
81 | describe('isHTMLLinkElement', () => {
82 | it('should return true for a HTMLLinkElement', () => {
83 | const link = document.createElement('link');
84 | const result = isHTMLLinkElement(link);
85 | expect(result).toBe(true);
86 | });
87 |
88 | it('should return false for a HTMLImageElement', () => {
89 | const img = new Image();
90 | const result = isHTMLLinkElement(img);
91 | expect(result).toBe(false);
92 | });
93 |
94 | it('should return false for a HTMLScriptElement', () => {
95 | const script = document.createElement('script');
96 | const result = isHTMLLinkElement(script);
97 | expect(result).toBe(false);
98 | });
99 |
100 | it('should return false for a HTMLAudioElement', () => {
101 | const audio = new Audio();
102 | const result = isHTMLLinkElement(audio);
103 | expect(result).toBe(false);
104 | });
105 |
106 | it('should return false for a HTMLVideoElement', () => {
107 | const video = document.createElement('video');
108 | const result = isHTMLLinkElement(video);
109 | expect(result).toBe(false);
110 | });
111 | });
112 |
113 |
114 |
115 |
116 |
117 |
--------------------------------------------------------------------------------
/src/usePrefetch/__tests__/index.spec.tsx:
--------------------------------------------------------------------------------
1 | import { renderHook, act } from '@testing-library/react';
2 | import { usePrefetch } from '../index';
3 |
4 | describe('Test usePrefetch', () => {
5 | it('should prefetch resources when the component mounts', async () => {
6 | const urls = ['https://example.com/image1.jpg', 'https://example.com/image2.jpg'];
7 | const prefetchSpy = jest.spyOn(window.HTMLImageElement.prototype, 'src', 'set');
8 |
9 | renderHook(() => usePrefetch(urls));
10 |
11 | await act(async () => {
12 | await new Promise(resolve => setTimeout(resolve, 100));
13 | });
14 |
15 | expect(prefetchSpy).toHaveBeenCalledTimes(2);
16 | expect(prefetchSpy).toHaveBeenCalledWith(urls[0]);
17 | expect(prefetchSpy).toHaveBeenCalledWith(urls[1]);
18 |
19 | prefetchSpy.mockRestore();
20 | });
21 | });
22 |
23 | describe('Test usePrefetch with mock Image', () => {
24 | let createImageSpy: jest.SpyInstance;
25 | let addEventListener: jest.Mock;
26 | let removeEventListener: jest.Mock;
27 |
28 | beforeEach(() => {
29 | addEventListener = jest.fn();
30 | removeEventListener = jest.fn();
31 | createImageSpy = jest.spyOn(global, 'Image').mockImplementation(() => ({
32 | addEventListener,
33 | removeEventListener,
34 | src: '',
35 | crossOrigin: '',
36 | }) as any);
37 | });
38 |
39 | afterEach(() => {
40 | createImageSpy.mockRestore();
41 | addEventListener.mockClear();
42 | removeEventListener.mockClear()
43 | });
44 |
45 | it('should load image correctly', () => {
46 | const urls = ['https://example.com/image1.jpg', 'https://example.com/image2.jpg'];
47 | renderHook(() => usePrefetch(urls));
48 | addEventListener.mock.calls[2][1]();
49 | expect(addEventListener).toHaveBeenCalledTimes(4);
50 | });
51 |
52 | it('should load image error', () => {
53 | const urls = ['https://example.com/image1.jpg', 'https://example.com/image2.jpg'];
54 | renderHook(() => usePrefetch(urls));
55 | addEventListener.mock.calls[3][1]();
56 | addEventListener.mock.calls[3][1]();
57 | addEventListener.mock.calls[3][1]();
58 | expect(addEventListener).toHaveBeenCalledTimes(4);
59 | });
60 | });
61 |
62 | describe('Test usePrefetch hit cache', () => {
63 | let createImageSpy: jest.SpyInstance;
64 | let addEventListener: jest.Mock;
65 | let removeEventListener: jest.Mock;
66 |
67 | beforeEach(() => {
68 | addEventListener = jest.fn();
69 | removeEventListener = jest.fn();
70 | createImageSpy = jest.spyOn(global, 'Image').mockImplementation(() => ({
71 | addEventListener,
72 | removeEventListener,
73 | src: '',
74 | crossOrigin: '',
75 | }) as any);
76 | });
77 |
78 | it('should hit cache', () => {
79 | const urls = ['https://example.com/image1.jpg', 'https://example.com/image1.jpg'];
80 | const { rerender } = renderHook(() => usePrefetch(urls));
81 | addEventListener.mock.calls[2][1]();
82 | rerender();
83 | expect(addEventListener).toHaveBeenCalledTimes(4);
84 | });
85 |
86 | afterEach(() => {
87 | createImageSpy.mockRestore();
88 | addEventListener.mockClear();
89 | removeEventListener.mockClear()
90 | });
91 | });
92 |
93 | describe('Test usePrefetch link', () => {
94 | it('should load link', () => {
95 | const addEventListenerSpy = jest.spyOn(HTMLLinkElement.prototype, 'addEventListener') as any;
96 |
97 | const url = 'https://example.com/image1.jpg';
98 | renderHook(() => usePrefetch(url, { type: 'link' }));
99 |
100 | addEventListenerSpy.mock.calls[1][1]?.();
101 | addEventListenerSpy.mock.calls[1][1]?.();
102 | addEventListenerSpy.mock.calls[1][1]?.();
103 | addEventListenerSpy.mock.calls[0][1]?.();
104 | addEventListenerSpy.mock.calls[1][1]?.();
105 |
106 | expect(addEventListenerSpy).toHaveBeenCalledTimes(2);
107 |
108 | });
109 | });
--------------------------------------------------------------------------------
/src/usePrefetch/createResource.ts:
--------------------------------------------------------------------------------
1 | export type ResourceType = 'image' | 'script' | 'link' | 'audio' | 'video';
2 |
3 | export type Resource = HTMLImageElement | HTMLScriptElement | HTMLLinkElement | HTMLAudioElement | HTMLVideoElement;
4 |
5 | function isHTMLLinkElement(element: Resource): element is HTMLLinkElement {
6 | return element instanceof HTMLLinkElement;
7 | }
8 |
9 | function createResource(url: string, type: ResourceType, crossOrigin?: 'anonymous' | 'use-credentials'): Resource {
10 | switch (type) {
11 | case 'image': {
12 | const img = new Image();
13 | img.src = url;
14 | if (crossOrigin) {
15 | img.crossOrigin = crossOrigin;
16 | }
17 | return img;
18 | }
19 | case 'script': {
20 | const script = document.createElement('script');
21 | script.src = url;
22 | if (crossOrigin) {
23 | script.crossOrigin = crossOrigin;
24 | }
25 | document.body.append(script);
26 | return script;
27 | }
28 | case 'link': {
29 | const link = document.createElement('link');
30 | link.href = url;
31 | link.rel = 'stylesheet';
32 | if (crossOrigin) {
33 | link.crossOrigin = crossOrigin;
34 | }
35 | document.head.append(link);
36 | return link;
37 | }
38 | case 'audio': {
39 | const audio = new Audio();
40 | audio.src = url;
41 | if (crossOrigin) {
42 | audio.crossOrigin = crossOrigin;
43 | }
44 | return audio;
45 | }
46 | case 'video': {
47 | const video = document.createElement('video');
48 | video.src = url;
49 | if (crossOrigin) {
50 | video.crossOrigin = crossOrigin;
51 | }
52 | return video;
53 | }
54 | default:
55 | throw new Error(`Invalid resource type: ${type}`);
56 | }
57 | }
58 |
59 | export {
60 | createResource,
61 | isHTMLLinkElement
62 | }
--------------------------------------------------------------------------------
/src/usePrefetch/demo/index.tsx:
--------------------------------------------------------------------------------
1 | import { usePrefetch } from '../index';
2 |
3 | /**
4 | * @input
5 | * import { usePrefetch } from 'microhook';
6 | */
7 |
8 | const images = [
9 | 'https://via.placeholder.com/150',
10 | 'https://via.placeholder.com/150/0000FF/808080',
11 | 'https://via.placeholder.com/150/FF0000/FFFFFF',
12 | ];
13 |
14 | function App() {
15 | usePrefetch(images, { type: 'image' });
16 |
17 | return (
18 |
19 |
Image Gallery
20 | {images.map((image, index) => (
21 |
22 | ))}
23 |
24 | );
25 | }
26 |
27 | export default App;
--------------------------------------------------------------------------------
/src/usePrefetch/index.tsx:
--------------------------------------------------------------------------------
1 | // Import dependencies
2 | import type { ResourceType, Resource } from './createResource';
3 | import { useEffect, useRef, useCallback } from 'react';
4 | import { createResource, isHTMLLinkElement } from './createResource';
5 | import { ReturnValue } from '../define';
6 |
7 | // Define options type
8 | type Options = {
9 | type?: ResourceType;
10 | crossOrigin?: 'anonymous' | 'use-credentials';
11 | maxRetryTimes?: number;
12 | maxConcurrent?: number;
13 | onLoad?: (resource: Resource) => void;
14 | };
15 |
16 | // Define resource item type
17 | type ResourceItem = {
18 | url: string;
19 | retryTimes: number;
20 | element: Resource;
21 | };
22 |
23 | // Define action interface
24 | interface Action { }
25 |
26 | // Define the usePrefetch hook
27 | function usePrefetch(urls: string[] | string, options: Options = {}): ReturnValue, Action> {
28 | // Destructure options
29 | const {
30 | type = 'image',
31 | crossOrigin,
32 | maxRetryTimes = 3,
33 | onLoad = () => { },
34 | } = options;
35 |
36 | // Create refs for cache and queue
37 | const cacheRef = useRef>({});
38 | const queueRef = useRef([]);
39 |
40 | // Define the handleLoad function
41 | const handleLoad = useCallback((resource: Resource) => {
42 | const item = queueRef.current.find((item) => item.element === resource);
43 | if (item) {
44 | // Add the loaded resource to the cache
45 | cacheRef.current[item.url] = resource;
46 | // Remove the item from the queue
47 | queueRef.current = queueRef.current.filter((queue) => queue !== item);
48 | // Call the onLoad callback
49 | onLoad(resource);
50 | }
51 | }, [onLoad]);
52 |
53 | // Define the handleError function
54 | const handleError = useCallback((resource: Resource) => {
55 | const item = queueRef.current.find((item) => item.element === resource);
56 | if (item) {
57 | // Increment the retry count
58 | item.retryTimes += 1;
59 | // Retry loading the resource if retry count is less than maxRetryTimes
60 | if (item.retryTimes < maxRetryTimes) {
61 | if (isHTMLLinkElement(item.element)) {
62 | item.element.href = `${item.url}?retry=${item.retryTimes}`;
63 | } else {
64 | item.element.src = `${item.url}?retry=${item.retryTimes}`;
65 | }
66 | } else {
67 | // Remove the item from the queue if retry count is equal to maxRetryTimes
68 | queueRef.current = queueRef.current.filter(queue => queue !== item);
69 | }
70 | }
71 | }, [maxRetryTimes]);
72 |
73 | // Add resources to the queue and listen for load and error events
74 | useEffect(() => {
75 | const urlsArr = Array.isArray(urls) ? urls : [urls];
76 | const handles = urlsArr.map((url) => {
77 | if (cacheRef.current[url]) {
78 | return {}
79 | }
80 |
81 | const resource = createResource(url, type, crossOrigin);
82 |
83 | const handleResourceLoad = () => handleLoad(resource);
84 | const handleResourceError = () => handleError(resource);
85 |
86 | resource.addEventListener('load', handleResourceLoad);
87 | resource.addEventListener('error', handleResourceError);
88 |
89 | const item: ResourceItem = { url, retryTimes: 0, element: resource };
90 | queueRef.current = [...queueRef.current, item];
91 | return {
92 | element: resource,
93 | handleResourceLoad,
94 | handleResourceError
95 | }
96 | });
97 |
98 | // Remove event listeners when component unmounts
99 | return () => {
100 | handles.forEach(handle => {
101 | handle?.element?.removeEventListener('load', handle.handleResourceLoad);
102 | handle?.element?.removeEventListener('error', handle.handleResourceError);
103 | })
104 | }
105 |
106 | }, [urls, crossOrigin, type, handleError, handleLoad]);
107 |
108 | return [cacheRef.current, {}]
109 | }
110 |
111 | export {
112 | usePrefetch
113 | }
114 |
115 |
116 |
--------------------------------------------------------------------------------
/src/useRestHeight/__tests__/index.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render, renderHook, screen } from "@testing-library/react";
3 |
4 | import { useRestHeight } from '../index';
5 |
6 | describe('Test useRestHeight', () => {
7 | beforeAll(() => {
8 | window.ResizeObserver =
9 | window.ResizeObserver ||
10 | function () {
11 | return {
12 | observe: jest.fn(),
13 | disconnect: jest.fn(),
14 | unobserve: jest.fn()
15 | }
16 | };
17 | });
18 |
19 | it('should have a height of 0 when parent container does not exist', () => {
20 | const { result } = renderHook(
21 | props => useRestHeight(props.parent), {
22 | initialProps: {
23 | parent: undefined
24 | }
25 | }
26 | );
27 | const [restHeight] = result.current;
28 | expect(restHeight).toEqual(0);
29 | });
30 |
31 | it('should have a height of 200 when parent container has a height of 200 and no child elements', () => {
32 | const getBoundingClientRect = jest.fn().mockReturnValue({ height: 200 });
33 | HTMLElement.prototype.getBoundingClientRect = getBoundingClientRect;
34 |
35 | render(
);
36 | const { result } = renderHook(
37 | props => useRestHeight(props.parent),
38 | {
39 | initialProps: {
40 | parent: '.parent'
41 | }
42 | }
43 | );
44 |
45 | expect(result.current[0]).toEqual(200);
46 | });
47 |
48 | it('should have a height of 140 when parent container has a height of 200 and two child elements have heights of 20 and 40 respectively', () => {
49 | const getBoundingClientRect = jest.fn()
50 | .mockReturnValue({ height: 200 })
51 | .mockReturnValueOnce({ height: 200 })
52 | .mockReturnValueOnce({ height: 20 })
53 | .mockReturnValueOnce({ height: 40 })
54 | .mockReturnValueOnce({ height: 200 })
55 | .mockReturnValueOnce({ height: 20 })
56 | .mockReturnValueOnce({ height: 40 });
57 | HTMLElement.prototype.getBoundingClientRect = getBoundingClientRect;
58 |
59 | render(
60 |
64 | );
65 | const { result } = renderHook(
66 | props => useRestHeight(props.parent, props.children),
67 | {
68 | initialProps: {
69 | parent: '.parent',
70 | children: ['.first', '.second']
71 | }
72 | }
73 | );
74 |
75 | expect(result.current[0]).toEqual(140);
76 | });
77 |
78 | it('should have a height of 140 when parent container has a height of 200 and two child elements have heights of 20 and 40 respectively and elements are accessed by ID', () => {
79 | const getBoundingClientRect = jest.fn()
80 | .mockReturnValue({ height: 200 })
81 | .mockReturnValueOnce({ height: 200 })
82 | .mockReturnValueOnce({ height: 20 })
83 | .mockReturnValueOnce({ height: 40 })
84 | .mockReturnValueOnce({ height: 200 })
85 | .mockReturnValueOnce({ height: 20 })
86 | .mockReturnValueOnce({ height: 40 })
87 | HTMLElement.prototype.getBoundingClientRect = getBoundingClientRect;
88 |
89 | render(
90 |
94 | );
95 | const { result } = renderHook(
96 | props => useRestHeight(props.parent, props.children),
97 | {
98 | initialProps: {
99 | parent: '.parent',
100 | children: ['.first', '#second', '#third']
101 | }
102 | }
103 | );
104 |
105 | expect(result.current[0]).toEqual(140);
106 | });
107 |
108 | it('when parent container has a height of 200, two child elements with heights of 20 and 40 respectively, custom offsets of 5 and 10, and a height of 125', () => {
109 | const getBoundingClientRect = jest.fn()
110 | .mockReturnValue({ height: 200 })
111 | .mockReturnValueOnce({ height: 200 })
112 | .mockReturnValueOnce({ height: 20 })
113 | .mockReturnValueOnce({ height: 40 })
114 | .mockReturnValueOnce({ height: 200 })
115 | .mockReturnValueOnce({ height: 20 })
116 | .mockReturnValueOnce({ height: 40 });
117 | HTMLElement.prototype.getBoundingClientRect = getBoundingClientRect;
118 |
119 | render(
120 |
124 | );
125 | const { result } = renderHook(
126 | props => useRestHeight(props.parent, props.children, props.offsets),
127 | {
128 | initialProps: {
129 | parent: '.parent',
130 | children: ['.first', '.second'],
131 | offsets: [5, 10]
132 | }
133 | }
134 | );
135 |
136 | expect(result.current[0]).toEqual(125);
137 | });
138 |
139 | it('simulates useRef', () => {
140 | const getBoundingClientRect = jest.fn().mockReturnValue({ height: 200 });
141 | HTMLElement.prototype.getBoundingClientRect = getBoundingClientRect;
142 |
143 | render(useRef
);
144 | const ref = { current: screen.getAllByText('useRef')[0] };
145 | const { result } = renderHook(
146 | props => useRestHeight(props.parent),
147 | {
148 | initialProps: {
149 | parent: ref
150 | }
151 | }
152 | );
153 |
154 | expect(result.current[0]).toEqual(200);
155 | });
156 | });
--------------------------------------------------------------------------------
/src/useRestHeight/demo/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react';
2 | import { useRestHeight } from '../index';
3 |
4 | /**
5 | * @input
6 | * import { useRestHeight } from 'microhook';
7 | */
8 |
9 | function Demo() {
10 | const parentRef = useRef(null);
11 | const child1Ref = useRef(null);
12 | const child2Ref = useRef(null);
13 |
14 | const [resetHeight] = useRestHeight(
15 | parentRef,
16 | [child1Ref, '.child2'],
17 | [1, 2, 3, 4]
18 | );
19 |
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 | {resetHeight}
28 |
29 |
30 | );
31 | }
32 |
33 | export {
34 | Demo
35 | }
--------------------------------------------------------------------------------
/src/useRestHeight/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Ruimve
3 | */
4 |
5 | import { useState, useEffect, RefObject } from 'react';
6 | import { ReturnValue } from '../define';
7 |
8 | type ElementArg = string | RefObject;
9 |
10 | interface Action {
11 | recalculateHeight: () => void;
12 | }
13 |
14 | /**
15 | * Gets the HTMLElement from a string or RefObject.
16 | *
17 | * @param element - The string selector or RefObject to get the HTMLElement from.
18 | * @returns The HTMLElement or null.
19 | */
20 | function getElement(element: ElementArg | null | undefined): HTMLElement | null {
21 | if (element instanceof Object && element.current instanceof HTMLElement) {
22 | return element.current;
23 | } else if (typeof element === 'string') {
24 | return document.querySelector(element);
25 | } else {
26 | return null;
27 | }
28 | }
29 |
30 | /**
31 | * Returns an array with the remaining height of the parent element
32 | * after subtracting the height of its child elements and any specified offsets.
33 | *
34 | * @param parent - The parent element or its RefObject.
35 | * @param children - The child elements or their RefObjects.
36 | * @param offsets - The offsets to subtract from the parent element's height.
37 | * @returns An array with the remaining height and a recalculateHeight function.
38 | */
39 | function useRestHeight(
40 | parent?: ElementArg,
41 | children?: ElementArg[],
42 | offsets: number[] = [],
43 | ): ReturnValue {
44 | const [restHeight, setRestHeight] = useState(0);
45 |
46 | /**
47 | * Calculates the remaining height of the parent element and sets the restHeight state.
48 | */
49 | const updateHeight = () => {
50 | const parentElement = getElement(parent);
51 | const childElements =
52 | children?.flatMap((child) =>
53 | Array.from(getElement(child) ? [getElement(child)!] : []),
54 | ) ?? [];
55 |
56 | if (!parentElement) {
57 | return;
58 | }
59 |
60 | const parentHeight = parentElement.getBoundingClientRect().height;
61 | const childHeight = childElements.reduce(
62 | (totalHeight, childRef) => totalHeight + childRef.getBoundingClientRect().height,
63 | 0,
64 | );
65 |
66 | const totalOffset = offsets.reduce((acc, curr) => acc + curr, 0);
67 | setRestHeight(parentHeight - childHeight - totalOffset);
68 | }
69 |
70 | /**
71 | * Sets up ResizeObservers on the parent and child elements to update the restHeight state.
72 | * Returns a cleanup function to disconnect the observers.
73 | */
74 | useEffect(() => {
75 | updateHeight();
76 |
77 | const parentElement = getElement(parent);
78 | const childElements =
79 | children?.flatMap((child) =>
80 | Array.from(getElement(child) ? [getElement(child)!] : []),
81 | ) ?? [];
82 |
83 | if (parentElement) {
84 | const parentObserver = new ResizeObserver(updateHeight);
85 | parentObserver.observe(parentElement);
86 |
87 | const childObservers = childElements.map((child) => {
88 | const observer = new ResizeObserver(updateHeight);
89 | observer.observe(child);
90 | return observer;
91 | });
92 |
93 | return () => {
94 | parentObserver.disconnect();
95 | childObservers.forEach((observer) => observer.disconnect());
96 | };
97 | }
98 | }, [parent, children, offsets]);
99 |
100 | return [restHeight, { recalculateHeight: updateHeight }];
101 | }
102 |
103 | export { useRestHeight };
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 |
13 | /* Language and Environment */
14 | "target": "ES2015", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | "jsx": "react-jsx", /* Specify what JSX code is generated. */
17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
26 |
27 | /* Modules */
28 | "module": "ES2015", /* Specify what module code is generated. */
29 | // "rootDir": "./", /* Specify the root folder within your source files. */
30 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
38 | // "resolveJsonModule": true, /* Enable importing .json files. */
39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
40 |
41 | /* JavaScript Support */
42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
45 |
46 | /* Emit */
47 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
52 | "outDir": "./types", /* Specify an output folder for all emitted files. */
53 | // "removeComments": true, /* Disable emitting comments. */
54 | // "noEmit": true, /* Disable emitting files from a compilation. */
55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
63 | // "newLine": "crlf", /* Set the newline character for emitting files. */
64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
68 | // "declarationDir": "./types", /* Specify the output directory for generated declaration files. */
69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
70 |
71 | /* Interop Constraints */
72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
74 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
77 |
78 | /* Type Checking */
79 | "strict": true, /* Enable all strict type-checking options. */
80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
81 | "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
98 |
99 | /* Completeness */
100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
102 | },
103 | "include": [
104 | "./src/**/*"
105 | ],
106 | "exclude": [
107 | "**/__tests__/**/*",
108 | "**/demo/**/*"
109 | ]
110 | }
111 |
--------------------------------------------------------------------------------