& { fallback: string };
4 |
5 | export const Image = ({ src, loading, alt, fallback, ...props }: Props) => {
6 | const handleBrokenImage = (event: any) => (event.target.src = fallback);
7 |
8 | return (
9 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/docs/demo/UseSingleEffectDemo.jsx:
--------------------------------------------------------------------------------
1 | import { useSingleEffect } from "react-haiku";
2 | import React from 'react';
3 |
4 | export const UseSingleEffectDemo = () => {
5 | const [renderCount, setRenderCount] = React.useState(0);
6 |
7 | useSingleEffect(() => {
8 | setRenderCount(renderCount + 1);
9 | }) // no dependency array needed
10 |
11 | return (
12 |
13 | Effect executed only {renderCount} time!
14 |
15 | );
16 | }
--------------------------------------------------------------------------------
/docs/demo/UseUrgentUpdateDemo.jsx:
--------------------------------------------------------------------------------
1 | import { useUrgentUpdate } from "react-haiku"
2 | import React from 'react';
3 |
4 | export const UseUrgentUpdateDemo = () => {
5 | const update = useUrgentUpdate();
6 | const randomNo = Math.floor(Math.random() * 100);
7 |
8 | return (
9 |
10 | {`Number: ${randomNo}`}
11 | Force Render
12 |
13 | );
14 | }
--------------------------------------------------------------------------------
/docs/demo/UseKeyPressDemo.jsx:
--------------------------------------------------------------------------------
1 | import { useKeyPress } from 'react-haiku';
2 | import React, { useState } from 'react';
3 |
4 | export const UseKeyPressDemo = () => {
5 | const [didKeyPress, setDidKeyPress] = useState(false);
6 | useKeyPress(['Control', 'Shift', 'A'], (e) => {
7 | setDidKeyPress(true);
8 | });
9 |
10 | return (
11 |
12 |
Press Control + Shift + A
13 | {didKeyPress &&
{`You pressed : Control + Shift + A`}
}
14 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/docs/demo/UseLeaveDetectionDemo.jsx:
--------------------------------------------------------------------------------
1 | import { useLeaveDetection } from "react-haiku"
2 | import React from 'react';
3 |
4 | export const UseLeaveDetectionDemo = () => {
5 | const [leaveCount, setLeaveCount] = React.useState(0);
6 | useLeaveDetection(() => setLeaveCount((s) => s + 1));
7 |
8 | return (
9 |
10 |
11 | {`You have left the page ${leaveCount} times!`}
12 |
13 |
14 | );
15 | }
--------------------------------------------------------------------------------
/docs/demo/UsePrefersThemeDemo.jsx:
--------------------------------------------------------------------------------
1 | import { usePrefersTheme } from "react-haiku"
2 | import React from 'react';
3 |
4 | export const UsePrefersThemeDemo = () => {
5 | const theme = usePrefersTheme();
6 |
7 | return (
8 |
9 |
10 | {
11 | `You prefer the ${theme} theme,
12 | ${theme === 'light' ? 'ew!' : 'great!'}`
13 | }
14 |
15 |
16 | );
17 | }
--------------------------------------------------------------------------------
/docs/src/components/Demo/ClickOutside.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useClickOutside } from "react-haiku"
3 |
4 | export const ClickOutside = () => {
5 | const [count, setCount] = React.useState(0);
6 | const ref = React.useRef(null)
7 |
8 | const handleClickOutside = () => setCount(count + 1);
9 |
10 | useClickOutside(ref, handleClickOutside);
11 |
12 | return (
13 |
14 | Clicked outside of this slide {count} time{count === 1 ? '' : 's'}!
15 |
16 | );
17 | }
--------------------------------------------------------------------------------
/docs/demo/UseScriptDemo.jsx:
--------------------------------------------------------------------------------
1 | import { useScript } from "react-haiku"
2 | import React from 'react';
3 |
4 | export const UseScriptDemo = () => {
5 | const script = useScript('https://cdnjs.cloudflare.com/ajax/libs/gsap/3.10.4/gsap.min.js');
6 |
7 | return (
8 |
9 | Check the bottom of the body tag!
10 |
11 | {`Script Status: ${script}`}
12 |
13 |
14 | );
15 | }
--------------------------------------------------------------------------------
/docs/demo/UseInputValueDemo.jsx:
--------------------------------------------------------------------------------
1 | import { useInputValue } from "react-haiku"
2 | import React from 'react';
3 |
4 | export const UseInputValueDemo = () => {
5 | const [nameValue, setNameValue] = useInputValue('');
6 |
7 | return (
8 |
9 |
14 |
{`Value - ${nameValue ? nameValue : 'None'}`}
15 |
16 | );
17 | }
--------------------------------------------------------------------------------
/docs/demo/UseScrollPositionDemo.jsx:
--------------------------------------------------------------------------------
1 | import { useScrollPosition } from "react-haiku";
2 | import React from 'react';
3 |
4 | export const UseScrollPositionDemo = () => {
5 | const [scroll, setScroll] = useScrollPosition();
6 |
7 | return (
8 |
9 | Current Position: {`X: ${scroll.x}, Y: ${scroll.y}`}!
10 | setScroll({ y: document.body.scrollHeight })} className="demo-button">Scroll To Bottom
11 |
12 | );
13 | }
--------------------------------------------------------------------------------
/docs/static/img/copy.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/lib/hooks/usePrevious.ts:
--------------------------------------------------------------------------------
1 | import { useRef } from 'react';
2 |
3 | import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';
4 |
5 | /**
6 | * Tracks the previous value of a given input.
7 | *
8 | * @param {T} value The current value to track.
9 | * @returns {T} The previous value of the input, or `value` if it's the first render.
10 | */
11 | export const usePrevious = (value: T): T => {
12 | const ref = useRef(value)
13 |
14 | useIsomorphicLayoutEffect(() => {
15 | ref.current = value
16 | }, [value])
17 |
18 | return ref.current
19 | }
20 |
--------------------------------------------------------------------------------
/docs/static/img/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/lib/hooks/useNetwork.ts:
--------------------------------------------------------------------------------
1 | import { useState, useCallback } from 'react';
2 | import { useEventListener } from './useEventListener';
3 |
4 | export function useNetwork() {
5 | const [isOnline, setIsOnline] = useState(navigator?.onLine || true);
6 |
7 | const handleOnline = useCallback(() => {
8 | setIsOnline(true);
9 | }, []);
10 |
11 | const handleOffline = useCallback(() => {
12 | setIsOnline(false);
13 | }, []);
14 |
15 | useEventListener('online', handleOnline);
16 | useEventListener('offline', handleOffline);
17 |
18 | return isOnline;
19 | }
20 |
--------------------------------------------------------------------------------
/lib/hooks/useToggle.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | export function useToggle(initialValue: T, options: [T, T]) {
4 | const [state, setState] = useState(initialValue);
5 | const handleToggle = () =>
6 | setState((current) => (current === options[0] ? options[1] : options[0]));
7 |
8 | const toggle = (value: T) =>
9 | typeof value !== 'undefined' ? setState(value) : handleToggle();
10 |
11 | return [state, toggle];
12 | }
13 |
14 | export function useBoolToggle(initialValue = false) {
15 | return useToggle(initialValue, [true, false]);
16 | }
17 |
--------------------------------------------------------------------------------
/docs/demo/UseMediaQueryDemo.jsx:
--------------------------------------------------------------------------------
1 | import { useMediaQuery } from "react-haiku"
2 | import React from 'react';
3 |
4 | export const UseMediaQueryDemo = () => {
5 | const breakpoint = useMediaQuery('(max-width: 1200px)');
6 |
7 | return (
8 |
9 |
Resize your window!
10 |
11 | {breakpoint ? "It's a match for 1200px!" : "Not a match for 1200px!"}
12 |
13 |
14 | );
15 | }
--------------------------------------------------------------------------------
/docs/demo/UsePreventBodyScrollDemo.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { usePreventBodyScroll } from 'react-haiku';
3 |
4 | export const UsePreventBodyScrollDemo = () => {
5 | const { isScrollLocked, toggleScrollLock } = usePreventBodyScroll();
6 |
7 | return (
8 |
9 |
10 | Scroll: {isScrollLocked ? 'Disabled' : 'Enabled'}
11 |
12 |
13 | Toggle Scroll!
14 |
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/docs/demo/UseWindowSizeDemo.jsx:
--------------------------------------------------------------------------------
1 | import { useWindowSize } from 'react-haiku';
2 | import React from 'react';
3 |
4 | export const UseWindowSizeDemo = () => {
5 | const {height, width} = useWindowSize();
6 | return (
7 |
8 |
Resize Your Window!
9 |
Window Height: {height}
10 |
Window Width: {width}
11 |
12 | );
13 | }
--------------------------------------------------------------------------------
/lib/hooks/useFavicon.ts:
--------------------------------------------------------------------------------
1 | export function useFavicon(defaultHref: string | null = null) {
2 | const set = (hrefToSet: string) => {
3 | const link: HTMLLinkElement | null =
4 | document.querySelector("link[rel*='icon']") ||
5 | document.createElement('link');
6 |
7 | link.type = 'image/x-icon';
8 | link.rel = 'shortcut icon';
9 | link.href = hrefToSet;
10 |
11 | document.getElementsByTagName('head')[0]?.appendChild(link);
12 | };
13 |
14 | const setFavicon = (href: string) =>
15 | defaultHref && !href ? set(defaultHref) : set(href);
16 |
17 | return { setFavicon };
18 | }
19 |
--------------------------------------------------------------------------------
/docs/demo/UseHoldDemo.jsx:
--------------------------------------------------------------------------------
1 |
2 | import { useHold } from "react-haiku"
3 | import React from 'react';
4 |
5 | export const UseHoldDemo = () => {
6 | const [count, setCount] = React.useState(0)
7 | const handleHold = () => setCount(count + 1);
8 |
9 | const buttonHold = useHold(handleHold, { delay: 2000 });
10 |
11 | return (
12 |
13 | Successful Holds: {count}
14 |
15 | Press & Hold!
16 |
17 |
18 | );
19 | }
--------------------------------------------------------------------------------
/docs/demo/UseConfirmExitDemo.jsx:
--------------------------------------------------------------------------------
1 |
2 | import { useConfirmExit, useBoolToggle } from "react-haiku"
3 | import React from 'react';
4 |
5 | export const UseConfirmExitDemo = () => {
6 | const [dirty, toggleDirty] = useBoolToggle();
7 | useConfirmExit(dirty);
8 |
9 | return (
10 |
11 |
Try to close this tab with the window dirty!
12 |
Dirty: {`${dirty}`}
13 |
toggleDirty()}>{dirty ? 'Set Clean' : 'Set Dirty'}
14 |
15 | );
16 | }
--------------------------------------------------------------------------------
/docs/demo/UseOrientationDemo.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useOrientation } from 'react-haiku';
3 |
4 | export const UseOrientationDemo = () => {
5 | const orientation = useOrientation();
6 |
7 | return (
8 |
9 |
Current Orientation:
10 |
18 | {orientation.toUpperCase()}
19 |
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/docs/demo/UseLocalStorageDemo.jsx:
--------------------------------------------------------------------------------
1 |
2 | import { useLocalStorage } from "react-haiku";
3 | import React from 'react';
4 |
5 | export const UseLocalStorageDemo = () => {
6 | const [value, setValue] = useLocalStorage('message');
7 |
8 | React.useEffect(() => {
9 | setValue({ message: 'Hello!' })
10 | }, [])
11 |
12 | return (
13 |
14 | Storage Value: {value?.message}
15 | setValue({ message: 'Woah!' })}>Update Storage
16 |
17 | );
18 | }
--------------------------------------------------------------------------------
/docs/demo/UseBatteryStatusDemo.jsx:
--------------------------------------------------------------------------------
1 | import { useBatteryStatus } from 'react-haiku';
2 | import React from 'react';
3 |
4 | export const UseBatteryStatusDemo = () => {
5 | const {level, isCharging} = useBatteryStatus();
6 |
7 | return (
8 |
9 |
Battery Status!
10 |
Battery Level: {level}
11 |
Is Battery Charging: {isCharging ? "True" : "False"}
12 |
13 | );
14 | }
--------------------------------------------------------------------------------
/docs/docs/hooks/useHover.mdx:
--------------------------------------------------------------------------------
1 | # useHover()
2 |
3 | The useHover() hook lets you detect if the user's mouse is hovering over an element
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useHover } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UseHoverDemo } from '../../demo/UseHoverDemo.jsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { useHover } from 'react-haiku';
19 |
20 | export const Component = () => {
21 | const { hovered, ref } = useHover();
22 |
23 | return(
24 |
25 | {hovered ? 'All mice on me!' : 'No mice on me!'}
26 |
27 | );
28 | }
29 | ```
--------------------------------------------------------------------------------
/docs/demo/UseClickOutsideDemo.jsx:
--------------------------------------------------------------------------------
1 |
2 | import { useClickOutside } from "react-haiku";
3 | import React from 'react';
4 |
5 | export const UseClickOutsideDemo = () => {
6 | const [count, setCount] = React.useState(0);
7 | const ref = React.useRef(null)
8 |
9 | const handleClickOutside = () => setCount(count + 1);
10 |
11 | useClickOutside(ref, handleClickOutside);
12 |
13 | return (
14 |
15 | Clicked Outside {count} Times!
16 | Click Outside Of Me!
17 |
18 | );
19 | }
--------------------------------------------------------------------------------
/docs/docs/utilities/renderAfter.mdx:
--------------------------------------------------------------------------------
1 | # RenderAfter
2 |
3 | The `RenderAfter` component can be used to render components or JSX code wrapped inside of it after a set delay.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { RenderAfter } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { RenderAfterDemo } from '../../demo/RenderAfterDemo.jsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { RenderAfter } from 'react-haiku';
19 |
20 | export const Component = () => {
21 | return(
22 |
23 | Wait 5 seconds and I'll show up!
24 |
25 | );
26 | }
27 | ```
28 |
--------------------------------------------------------------------------------
/lib/tests/utils/If.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import { If } from '../../utils/If';
3 |
4 | describe('If', () => {
5 | const text = 'If Block';
6 |
7 | it('renders the If component', () => {
8 | const { asFragment } = render({text} );
9 | expect(asFragment()).toMatchSnapshot();
10 | expect(screen.getByText(text)).toBeInTheDocument();
11 | });
12 |
13 | it('should not render anything if isTrue is false', () => {
14 | render({text} );
15 | expect(screen.queryByText(text)).toBeNull();
16 | expect(screen.queryByText(text)).not.toBeInTheDocument();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/docs/docs/hooks/useFavicon.mdx:
--------------------------------------------------------------------------------
1 | # useFavicon()
2 |
3 | The `useFavicon()` hook lets change the website's favicon dynamically from your components! The favicon changes back to the default one on refresh!
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useFavicon } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UseFaviconDemo } from '../../demo/UseFaviconDemo.jsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { useFavicon } from "react-haiku"
19 |
20 | export const Component = () => {
21 | const { setFavicon } = useFavicon();
22 |
23 | return setFavicon('https://bit.ly/3NEz8Sj')}>Update Favicon
24 | }
25 | ```
--------------------------------------------------------------------------------
/lib/hooks/useLeaveDetection.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 |
3 | export function useLeaveDetection(
4 | onLeave: (this: HTMLElement, ev: MouseEvent) => any,
5 | ) {
6 | const onLeaveRef = useRef(onLeave);
7 |
8 | useEffect(() => {
9 | onLeaveRef.current = onLeave;
10 | }, [onLeave]);
11 |
12 | useEffect(() => {
13 | const handler = function (this: HTMLElement, ev: MouseEvent) {
14 | onLeaveRef.current.call(this, ev);
15 | };
16 |
17 | document.documentElement.addEventListener('mouseleave', handler);
18 |
19 | return () =>
20 | document.documentElement.removeEventListener('mouseleave', handler);
21 | }, []);
22 | }
23 |
--------------------------------------------------------------------------------
/docs/demo/UseMousePositionDemo.jsx:
--------------------------------------------------------------------------------
1 | import { useMousePosition } from "react-haiku"
2 | import React from 'react';
3 |
4 | export const UseMousePositionDemo = () => {
5 | const { target, x, y } = useMousePosition();
6 |
7 | return (
8 |
9 |
Hover This Container
10 |
{`X: ${x} | Y: ${y}`}
11 |
12 | );
13 | }
14 |
15 | export const UseMousePositionFullDemo = () => {
16 | const { x, y } = useMousePosition();
17 |
18 | return (
19 |
20 |
{`X: ${x} | Y: ${y}`}
21 |
22 | );
23 | }
--------------------------------------------------------------------------------
/docs/demo/UseFirstRenderDemo.jsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 |
4 | /* The functionality in this demo had to be emulated due to Docusaurus re-rendering
5 | my Demo component multiple times and not allowing the showcase of the hook */
6 |
7 | export const UseFirstRenderDemo = () => {
8 | const [isFirst, setFirst] = React.useState(true);
9 |
10 | return (
11 |
12 | First Render? - {isFirst ? 'Yes' : 'No'}
13 | setFirst(false)}>
14 | Trigger Re-Render
15 |
16 |
17 | );
18 | }
--------------------------------------------------------------------------------
/lib/hooks/useSingleEffect.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 |
3 | export function useSingleEffect(effect: () => void | (() => void)) {
4 | const destroy = useRef void)>(undefined);
5 | const calledOnce = useRef(false);
6 | const renderAfterCalled = useRef(false);
7 |
8 | if (calledOnce.current) renderAfterCalled.current = true;
9 |
10 | useEffect(() => {
11 | if (calledOnce.current) {
12 | return;
13 | }
14 |
15 | calledOnce.current = true;
16 | destroy.current = effect();
17 |
18 | return () => {
19 | if (!renderAfterCalled.current) {
20 | return;
21 | }
22 | if (destroy.current) destroy.current();
23 | };
24 | }, [effect]);
25 | }
26 |
--------------------------------------------------------------------------------
/docs/src/components/Demo/Debounce.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useDebounce } from "react-haiku"
3 |
4 | export const Debounce = () => {
5 | const [value, setValue] = React.useState('')
6 | const debouncedValue = useDebounce(value, 1000)
7 |
8 | const handleChange = (event) => setValue(event.target.value)
9 |
10 | return (
11 |
12 |
13 | Realtime Value:{value}
14 | Debounce value:{debouncedValue}
15 |
16 |
17 |
18 |
19 | );
20 | }
--------------------------------------------------------------------------------
/docs/demo/IfDemo.jsx:
--------------------------------------------------------------------------------
1 | import { If } from "react-haiku"
2 | import React from 'react';
3 |
4 | export const IfDemo = () => {
5 | const [number, setNumber] = React.useState(6);
6 |
7 | return(
8 |
9 | Click to update state!
10 |
11 | setNumber(7)}>I like the number 6!
12 |
13 |
14 | setNumber(6)}>7 is way better!
15 |
16 |
17 | );
18 | }
--------------------------------------------------------------------------------
/docs/demo/UseUpdateEffectDemo.jsx:
--------------------------------------------------------------------------------
1 | import { useUpdateEffect } from "react-haiku";
2 | import React from 'react';
3 |
4 | export const UseUpdateEffectDemo = () => {
5 | const [count, setCount] = React.useState(0);
6 | const [triggerCount, setTriggerCount] = React.useState(0);
7 |
8 | useUpdateEffect(() => {
9 | setTriggerCount(triggerCount + 1);
10 | }, [count]) // no dependency array needed
11 |
12 | return (
13 |
14 | Updates Detected: {triggerCount}
15 | setCount(count + 1)} className="demo-button">Update State
16 |
17 | );
18 | }
--------------------------------------------------------------------------------
/lib/tests/hooks/useFirstRender.test.tsx:
--------------------------------------------------------------------------------
1 | import { renderHook } from '@testing-library/react';
2 | import { useFirstRender } from '../../hooks/useFirstRender';
3 |
4 | describe('useFirstRender', () => {
5 | it('should return true on first render', () => {
6 | const { result } = renderHook(() => useFirstRender());
7 |
8 | expect(result.current).toBe(true);
9 | });
10 |
11 | it('should maintain false state across multiple rerenders', () => {
12 | const { result, rerender } = renderHook(() => useFirstRender());
13 |
14 | // Multiple subsequent renders.
15 | for (let i = 0; i < 5; i++) {
16 | rerender();
17 |
18 | expect(result.current).toBe(false);
19 | }
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/docs/demo/UseIdleDemo.jsx:
--------------------------------------------------------------------------------
1 | import { useIdle } from "react-haiku";
2 | import React from 'react';
3 |
4 | export const UseIdleDemo = () => {
5 | const idle = useIdle(3000);
6 |
7 | return (
8 |
9 | Current Status: {idle ? 'Idle' : 'Active'}
10 |
11 | );
12 | }
13 |
14 | export const UseIdleCustomDemo = () => {
15 | const idle = useIdle(1000, { events: ['click', 'touchstart'], initialState: false });
16 |
17 | return (
18 |
19 | Works only with click/touch events!
20 | Current Status: {idle ? 'Idle' : 'Active'}
21 |
22 | );
23 | }
--------------------------------------------------------------------------------
/docs/demo/UseFullscreenDemo.jsx:
--------------------------------------------------------------------------------
1 | import { useFullscreen } from 'react-haiku'
2 | import React from 'react'
3 |
4 | export const UseFullscreenDemo = () => {
5 | const documentRef = React.useRef(null);
6 |
7 | React.useEffect(() => {
8 | documentRef.current = document.documentElement;
9 | }, []);
10 |
11 | const {isFullscreen, toggleFullscreen } = useFullscreen(documentRef);
12 | return (
13 |
14 | Is in Fullscreen Mode: {isFullscreen ? "True" : "False"}
15 | Toggle Fullscreen!
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/lib/tests/utils/For.test.tsx:
--------------------------------------------------------------------------------
1 | import { For } from '../../utils/For';
2 | import { render, screen } from '@testing-library/react';
3 |
4 | describe('For', () => {
5 | const testArray = ['foo', 'bar', 'baz'];
6 |
7 | it('renders the For component', () => {
8 | const { asFragment } = render(
9 | {`${index}: ${item}`} }
12 | />,
13 | );
14 | expect(asFragment()).toMatchSnapshot();
15 |
16 | testArray.forEach((item, index) => {
17 | expect(screen.getByText(`${index}: ${item}`)).toBeInTheDocument();
18 | });
19 | expect(document.body.firstChild!.childNodes).toHaveLength(testArray.length);
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/docs/demo/UseIntersectionObserverDemo.jsx:
--------------------------------------------------------------------------------
1 | import { useIntersectionObserver } from 'react-haiku';
2 | import React from 'react';
3 |
4 | export const UseIntersectionObserverDemo = () => {
5 | const {observeRef, isVisible} = useIntersectionObserver({
6 | animateOnce: false,
7 | options:{
8 | threshold: .5,
9 | rootMargin: '-40% 0px -40% 0px'
10 | }
11 | })
12 | return (
13 |
14 |
Scroll Your Window!
15 |
16 | We {isVisible? 'are': 'are not'} intersecting!
17 |
18 |
19 | );
20 | }
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "jest";
2 |
3 | const config: Config = {
4 | collectCoverage: true,
5 | collectCoverageFrom: ["lib/**/*.{ts,tsx}", "!lib/**/*.d.ts", "!**/vendor/**"],
6 | coverageDirectory: "coverage",
7 | testEnvironment: "jsdom",
8 | testMatch: ["**/?(*.)+(spec|test).[jt]s?(x)"],
9 | transform: {
10 | ".(ts|tsx)": "ts-jest",
11 | },
12 |
13 | coveragePathIgnorePatterns: [
14 | "/node_modules/",
15 | "/coverage",
16 | "package.json",
17 | "package-lock.json",
18 | "reportWebVitals.ts",
19 | "jest.setup.ts",
20 | "index.tsx",
21 | ],
22 | setupFilesAfterEnv: ["/jest.setup.ts"],
23 | snapshotResolver: "/snapshotResolver.ts",
24 | };
25 |
26 | export default config;
27 |
--------------------------------------------------------------------------------
/snapshotResolver.ts:
--------------------------------------------------------------------------------
1 | import type { SnapshotResolver } from "jest-snapshot";
2 |
3 | const snapshotResolver: SnapshotResolver = {
4 | resolveSnapshotPath: (
5 | testPath: string,
6 | snapshotExtension: string
7 | ): string => {
8 | // Stores the snapshot next to the test file, replacing `.test.tsx` with `.test.tsx.snap`
9 | return testPath + snapshotExtension;
10 | },
11 | resolveTestPath: (
12 | snapshotFilePath: string,
13 | snapshotExtension: string
14 | ): string => {
15 | // Reverts the snapshot file path back to the test file path
16 | return snapshotFilePath.slice(0, -snapshotExtension.length);
17 | },
18 | testPathForConsistencyCheck: "some/example.test.tsx",
19 | };
20 |
21 | export default snapshotResolver;
22 |
--------------------------------------------------------------------------------
/docs/demo/ClassDemo.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Class } from 'react-haiku';
3 |
4 | export const ClassDemo = () => {
5 | const [isActive, setIsActive] = React.useState(false);
6 |
7 | return (
8 |
9 | setIsActive(!isActive)}
12 | style={{ marginBottom: '1em' }}
13 | >
14 | {isActive ? 'Remove Active class' : 'Add Active class'}
15 |
16 |
22 | This is a Class component
23 |
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/docs/src/components/Demo/Hold.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useHold } from "react-haiku"
3 |
4 | export const Hold = () => {
5 | const [count, setCount] = React.useState(0)
6 | const [holdText, setHoldText] = React.useState('Press & Hold!');
7 | const handleHold = () => {
8 | setCount(count + 1);
9 | setHoldText('Let go!');
10 |
11 | setTimeout(() => {
12 | setHoldText('Press & Hold!')
13 | }, 1000)
14 | }
15 |
16 | const buttonHold = useHold(handleHold, { delay: 2000 });
17 |
18 | return (
19 |
20 | Successfully held{count} time{count === 1 ? '' : 's'}!
21 |
22 | {holdText}
23 |
24 |
25 | );
26 | }
--------------------------------------------------------------------------------
/docs/demo/UseDebounceDemo.jsx:
--------------------------------------------------------------------------------
1 |
2 | import { useDebounce } from "react-haiku"
3 | import React from 'react';
4 |
5 | export const UseDebounceDemo = () => {
6 | const [value, setValue] = React.useState('')
7 | const debouncedValue = useDebounce(value, 1000)
8 |
9 | const handleChange = (event) => setValue(event.target.value)
10 |
11 | React.useEffect(() => {
12 | console.log(debouncedValue);
13 | }, [debouncedValue])
14 |
15 | return (
16 |
17 | Real-Time Value: {value}
18 | Debounced value: {debouncedValue}
19 |
20 |
21 |
22 | );
23 | }
--------------------------------------------------------------------------------
/docs/docs/hooks/usePrefersTheme.mdx:
--------------------------------------------------------------------------------
1 | # usePrefersTheme()
2 |
3 | The `usePrefersTheme()` hook allows the detection of the user's preferred system theme
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { usePrefersTheme } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UsePrefersThemeDemo } from '../../demo/UsePrefersThemeDemo.jsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { usePrefersTheme } from 'react-haiku';
19 |
20 | export const Component = () => {
21 | const theme = usePrefersTheme();
22 |
23 | return (
24 |
25 | {
26 | `You prefer the ${theme} theme,
27 | ${theme === 'light' ? 'ew!' : 'great!'}`
28 | }
29 |
30 | );
31 | }
32 | ```
--------------------------------------------------------------------------------
/lib/hooks/useInputValue.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | export function handleChange(setValue: React.Dispatch) {
4 | return (val: T) => {
5 | if (!val) {
6 | setValue(val);
7 | } else if (typeof val === 'object' && 'nativeEvent' in val) {
8 | // @ts-ignore
9 | const { currentTarget } = val;
10 |
11 | if (currentTarget.type === 'checkbox') {
12 | setValue(currentTarget.checked);
13 | } else {
14 | setValue(currentTarget.value);
15 | }
16 | } else {
17 | setValue(val);
18 | }
19 | };
20 | }
21 |
22 | export function useInputValue(initialState: T) {
23 | const [value, setValue] = useState(initialState);
24 |
25 | return [value, handleChange(setValue)] as const;
26 | }
27 |
--------------------------------------------------------------------------------
/lib/tests/hooks/useEventListener.test.tsx:
--------------------------------------------------------------------------------
1 | import { renderHook, act } from '@testing-library/react';
2 | import { useEventListener } from '../../hooks/useEventListener';
3 |
4 | describe('useEventListener Hook', () => {
5 | test('adds and triggers event listener correctly', () => {
6 | const handler = jest.fn();
7 |
8 | const { unmount } = renderHook(() => useEventListener('click', handler));
9 |
10 | act(() => {
11 | window.dispatchEvent(new MouseEvent('click'));
12 | });
13 |
14 | expect(handler).toHaveBeenCalledTimes(1);
15 |
16 | unmount();
17 |
18 | act(() => {
19 | window.dispatchEvent(new MouseEvent('click'));
20 | });
21 |
22 | expect(handler).toHaveBeenCalledTimes(1); // Should not trigger after unmount
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/docs/docs/hooks/useTitle.mdx:
--------------------------------------------------------------------------------
1 | # useTitle()
2 |
3 | The `useTitle()` hook allows the dynamic update of the document's title from your React components! The title passed to this hook can be attached to a piece of state, updating the state will therefore also update the title.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useTitle } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UseTitleDemo } from '../../demo/UseTitleDemo.jsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { useState } from 'react';
19 | import { useTitle } from 'react-haiku';
20 |
21 | export const Component = () => {
22 | const [title, setTitle] = useState('');
23 | useTitle(title);
24 |
25 | return setTitle('It works!')}>Update Title
26 | }
27 | ```
--------------------------------------------------------------------------------
/lib/hooks/useSize.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | export const useSize = (ref: any) => {
4 | const [dimensions, setDimensions] = useState({
5 | width: 0,
6 | height: 0,
7 | });
8 |
9 | useEffect(() => {
10 | if (!ref.current) {
11 | return;
12 | }
13 |
14 | const updateDimensions = () => {
15 | const { clientWidth, clientHeight } = ref.current;
16 | setDimensions({ width: clientWidth, height: clientHeight });
17 | };
18 |
19 | const resizeObserver = new ResizeObserver(updateDimensions);
20 |
21 | updateDimensions();
22 |
23 | resizeObserver.observe(ref.current);
24 |
25 | return () => {
26 | resizeObserver.disconnect();
27 | };
28 | }, [ref.current]);
29 |
30 | return dimensions;
31 | };
32 |
--------------------------------------------------------------------------------
/docs/docs/hooks/useUrgentUpdate.mdx:
--------------------------------------------------------------------------------
1 | # useUrgentUpdate()
2 |
3 | The `useUrgentUpdate()` hook forces a component to re-render when it gets called from anywhere inside it.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useUrgentUpdate } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UseUrgentUpdateDemo } from '../../demo/UseUrgentUpdateDemo.jsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { useUrgentUpdate } from 'react-haiku';
19 |
20 | export const Component = () => {
21 | const randomNo = Math.floor(Math.random() * 100);
22 | const update = useUrgentUpdate();
23 |
24 | return (
25 | <>
26 | {`Number: ${randomNo}`}
27 | Force Render
28 | >
29 | );
30 | }
31 | ```
--------------------------------------------------------------------------------
/docs/docs/hooks/useScrollDevice.mdx:
--------------------------------------------------------------------------------
1 | # useScrollDevice()
2 |
3 | The useScrollDevice() hook detects whether the user is scrolling with a mouse wheel or trackpad. Use this to adapt scroll behaviors or animations based on input device.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useScrollDevice } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UseScrollDeviceDemo } from '../../demo/UseScrollDeviceDemo.tsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { useScrollDevice } from 'react-haiku';
19 |
20 | export const Component = () => {
21 | const device = useScrollDevice();
22 |
23 | return Detected scroll device: {device || 'unknown'}
;
24 | };
25 | ```
26 |
27 | ### API
28 |
29 | #### Returns
30 |
31 | - `scrollDevice` - Detected device type: "mouse", "trackpad", or null
32 |
--------------------------------------------------------------------------------
/docs/static/img/copy-success.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/lib/hooks/useWindowSize.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 |
3 | type WindowSizeProps = {
4 | width: number;
5 | height: number;
6 | };
7 |
8 | export const useWindowSize = (): WindowSizeProps => {
9 | const [windowSize, setWindowSize] = useState({
10 | width: window?.innerWidth || 0,
11 | height: window?.innerHeight || 0
12 | });
13 |
14 | useEffect(() => {
15 | const handleResize = () => {
16 | if (typeof window !== "undefined") {
17 | setWindowSize({
18 | width: window.innerWidth,
19 | height: window.innerHeight
20 | })
21 | }
22 | }
23 |
24 | window.addEventListener("resize", handleResize);
25 | return () => window.removeEventListener("resize", handleResize);
26 | }, [])
27 |
28 | return windowSize;
29 | }
--------------------------------------------------------------------------------
/docs/demo/UseScreenSizeDemo.jsx:
--------------------------------------------------------------------------------
1 | import { useScreenSize } from 'react-haiku';
2 | import React from 'react';
3 |
4 |
5 | export const UseScreenSizeDemo = () => {
6 | const screenSize = useScreenSize();
7 |
8 | return (
9 |
10 |
Current Screen Size: {screenSize.toString()}
11 |
Is the screen size medium ? {screenSize.eq("md") ? "Yes" : "No"}
12 |
Is the screen size less than large ? {screenSize.lt("lg") ? "Yes" : "No"}
13 |
Is the screen size greater than small ? {screenSize.gt("sm") ? "Yes" : "No"}
14 |
Is the screen size greater than or equal to small ? {screenSize.gte("sm") ? "Yes" : "No"}
15 |
Is the screen size less than or equal to small ? {screenSize.lte("sm") ? "Yes" : "No"}
16 |
17 | );
18 | }
--------------------------------------------------------------------------------
/docs/docs/hooks/useLeaveDetection.mdx:
--------------------------------------------------------------------------------
1 | # useLeaveDetection()
2 |
3 | The useLeaveDetection() hook allows you to detect when a user's cursor leaves the document's boundaries
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useLeaveDetection } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UseLeaveDetectionDemo } from '../../demo/UseLeaveDetectionDemo.jsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { useLeaveDetection } from 'react-haiku';
19 | import { useState } from 'react';
20 |
21 | export const Component = () => {
22 | const [leaveCount, setLeaveCount] = useState(0);
23 | useLeaveDetection(() => setLeaveCount((s) => s + 1));
24 |
25 | return (
26 |
27 | {`You have left the page ${leaveCount} times!`}
28 |
29 | );
30 | }
31 | ```
--------------------------------------------------------------------------------
/lib/hooks/useHover.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef, useCallback } from 'react';
2 |
3 | export function useHover() {
4 | const [hovered, setHovered] = useState(false);
5 | const ref = useRef(null);
6 | const onMouseEnter = useCallback(() => setHovered(true), []);
7 | const onMouseLeave = useCallback(() => setHovered(false), []);
8 |
9 | useEffect(() => {
10 | if (ref.current) {
11 | ref.current.addEventListener('mouseenter', onMouseEnter);
12 | ref.current.addEventListener('mouseleave', onMouseLeave);
13 |
14 | return () => {
15 | ref.current?.removeEventListener('mouseenter', onMouseEnter);
16 | ref.current?.removeEventListener('mouseleave', onMouseLeave);
17 | };
18 | }
19 |
20 | return undefined;
21 | }, []);
22 |
23 | return { ref, hovered };
24 | }
25 |
--------------------------------------------------------------------------------
/docs/docs/utilities/for.mdx:
--------------------------------------------------------------------------------
1 | # For
2 |
3 | The `For` component can iterate over arrays and render JSX for each available item. Keys are automatically assigned.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { For } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { ForDemo } from '../../demo/ForDemo.jsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { For } from 'react-haiku';
19 |
20 | export const Component = () => {
21 | const data = [{name: 'React'}, {name: 'Haiku'}];
22 |
23 | return(
24 |
25 | {`${index}: ${item.name}`}
26 | }/>
27 | );
28 | }
29 | ```
30 |
31 | ### API
32 |
33 | The component accepts the following props:
34 |
35 | - `each` - the array to iterate over when rendering
36 | - `render` - the render method that can return JSX
--------------------------------------------------------------------------------
/lib/hooks/useOrientation.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 |
3 | type Orientation = "portrait" | "landscape";
4 |
5 | export const useOrientation = (): Orientation => {
6 | const [orientation, setOrientation] = useState(window.matchMedia("(orientation: portrait)").matches
7 | ? "portrait"
8 | : "landscape");
9 |
10 | useEffect(() => {
11 | const handleOrientationChange = (e: MediaQueryListEvent) => {
12 | setOrientation(e.matches ? "portrait" : "landscape");
13 | };
14 |
15 | const portraitMediaQuery = window.matchMedia("(orientation: portrait)");
16 | portraitMediaQuery.addEventListener("change", handleOrientationChange);
17 |
18 | return () =>
19 | portraitMediaQuery.removeEventListener("change", handleOrientationChange);
20 | }, []);
21 |
22 | return orientation;
23 | };
24 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "ESNext",
5 | "moduleResolution": "node",
6 | "jsx": "react-jsx",
7 | "rootDir": "./lib",
8 | "baseUrl": "./",
9 | "allowJs": true,
10 | "checkJs": true,
11 | "declaration": true,
12 | "outDir": "./dist",
13 | "esModuleInterop": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "strict": true,
16 | "noImplicitAny": true,
17 | "strictNullChecks": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noUncheckedIndexedAccess": true,
21 | "allowUnusedLabels": false,
22 | "allowUnreachableCode": false,
23 | "skipLibCheck": true,
24 | "types": ["@testing-library/jest-dom"]
25 | },
26 | "include": ["lib/**/*"],
27 | "exclude": ["node_modules", "dist", "docs"]
28 | }
29 |
--------------------------------------------------------------------------------
/docs/docs/hooks/useFirstRender.mdx:
--------------------------------------------------------------------------------
1 | # useFirstRender()
2 |
3 | The `useFirstRender()` hook lets you detect whether or not the component you use it on is on its initial render, it returns a boolean value with the result.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useFirstRender } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UseFirstRenderDemo } from '../../demo/UseFirstRenderDemo.jsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { useFirstRender, useUrgentUpdate } from "react-haiku"
19 |
20 | export const Component = () => {
21 | const isFirst = useFirstRender()
22 | const update = useUrgentUpdate();
23 |
24 | return (
25 | <>
26 | First Render? - {isFirst ? 'Yes' : 'No'}
27 | Trigger Re-Render
28 | >
29 | );
30 | }
31 | ```
--------------------------------------------------------------------------------
/docs/docs/hooks/useInputValue.mdx:
--------------------------------------------------------------------------------
1 | # useInputValue()
2 |
3 | The useInputValue() hook lets you easily manage states for inputs in your components
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useInputValue } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UseInputValueDemo } from '../../demo/UseInputValueDemo.jsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { useInputValue } from 'react-haiku';
19 |
20 | export const Component = () => {
21 | const [nameValue, setNameValue] = useInputValue('');
22 |
23 | return (
24 | <>
25 |
30 | {`Value - ${nameValue ? nameValue : 'None'}`}
31 | >
32 | );
33 | }
34 | ```
--------------------------------------------------------------------------------
/docs/sidebars.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Creating a sidebar enables you to:
3 | - create an ordered group of docs
4 | - render a sidebar for each doc of that group
5 | - provide next/previous navigation
6 |
7 | The sidebars can be generated from the filesystem, or explicitly defined here.
8 |
9 | Create as many sidebars as you want.
10 | */
11 |
12 | // @ts-check
13 |
14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
15 | const sidebars = {
16 | // By default, Docusaurus generates a sidebar from the docs folder structure
17 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
18 |
19 | // But you can create a sidebar manually
20 | /*
21 | tutorialSidebar: [
22 | {
23 | type: 'category',
24 | label: 'Tutorial',
25 | items: ['hello'],
26 | },
27 | ],
28 | */
29 | };
30 |
31 | module.exports = sidebars;
32 |
--------------------------------------------------------------------------------
/docs/docs/hooks/useDeviceOS.mdx:
--------------------------------------------------------------------------------
1 | # useDeviceOS()
2 |
3 | The `useDeviceOS()` hook detects the user's operating system, including mobile emulators, and uses string manipulation for identifying unique or new OS versions.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useDeviceOS } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import BrowserOnly from '@docusaurus/BrowserOnly';
14 | import { UseDeviceOSDemo } from '../../demo/UseDeviceOSDemo.jsx';
15 |
16 | Loading...}>
17 | {() => }
18 |
19 |
20 | ```jsx
21 |
22 | import { useDeviceOS } from 'react-haiku';
23 |
24 | export const Component = () => {
25 | const deviceOS = useDeviceOS();
26 |
27 | return (
28 |
29 |
Check Your Device OS!
30 |
OS: {deviceOS}
31 |
32 | );
33 | }
34 |
35 | ```
--------------------------------------------------------------------------------
/docs/docs/hooks/useWindowSize.mdx:
--------------------------------------------------------------------------------
1 | # useWindowSize()
2 |
3 | The `useWindowSize()` hook provides the current window `width` and `height` dimensions.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useWindowSize } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import BrowserOnly from '@docusaurus/BrowserOnly';
14 | import { UseWindowSizeDemo } from '../../demo/UseWindowSizeDemo.jsx';
15 |
16 | Loading...}>
17 | {() => }
18 |
19 |
20 | ```jsx
21 | import { useWindowSize } from 'react-haiku';
22 |
23 | export const Component = () => {
24 | const { height, width } = useWindowSize();
25 |
26 | return (
27 |
28 |
Resize Your Window!
29 |
30 |
Window Height: {height}
31 |
Window Width: {width}
32 |
33 | );
34 | };
35 | ```
36 |
--------------------------------------------------------------------------------
/lib/utils/Show.tsx:
--------------------------------------------------------------------------------
1 | import { Children } from 'react';
2 | import { If } from './If';
3 |
4 | import { type ReactNode } from 'react';
5 |
6 | type Props = {
7 | children: ReactNode & {
8 | props: {
9 | isTrue?: boolean;
10 | };
11 | };
12 | };
13 |
14 | type ElseProps = {
15 | render?: () => ReactNode;
16 | children?: ReactNode;
17 | };
18 |
19 | export const Show = ({ children }: Props) => {
20 | let when: ReactNode | null = null;
21 | let otherwise: ReactNode | null = null;
22 |
23 | Children.forEach(children, (child) => {
24 | if (child.props.isTrue === undefined) {
25 | otherwise = child;
26 | } else if (!when && child.props.isTrue === true) {
27 | when = child;
28 | }
29 | });
30 |
31 | return (when || otherwise) as ReactNode;
32 | };
33 |
34 | Show.When = If;
35 | Show.Else = ({ render, children }: ElseProps) => (render ? render() : children);
36 |
--------------------------------------------------------------------------------
/docs/demo/UseNetworkDemo.tsx:
--------------------------------------------------------------------------------
1 | import { useNetwork } from 'react-haiku';
2 | import React from 'react';
3 |
4 | export const UseNetworkDemo = () => {
5 | const isOnline = useNetwork();
6 |
7 | return (
8 |
9 |
Network Status:
10 |
15 | {isOnline ? 'ONLINE' : 'OFFLINE'}
16 |
17 |
26 | Toggle network status in Chrome DevTools (Application → Service Workers
27 | → Offline)
28 |
29 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/docs/docs/hooks/useSingleEffect.mdx:
--------------------------------------------------------------------------------
1 | # useSingleEffect()
2 |
3 | The `useSingleEffect()` hook works exactly like useEffect, except it is called only a single time when the component mounts. This helps with React's recent update to the useEffect hook which is being called twice on mount.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useSingleEffect } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UseSingleEffectDemo } from '../../demo/UseSingleEffectDemo.jsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { useState } from 'react';
19 | import { useSingleEffect } from 'react-haiku';
20 |
21 | export const Component = () => {
22 | const [renderCount, setRenderCount] = useState(0);
23 |
24 | useSingleEffect(() => {
25 | setRenderCount(renderCount + 1);
26 | }) // no dependency array needed
27 |
28 | return Effect executed only {renderCount} time!
29 | }
30 | ```
--------------------------------------------------------------------------------
/docs/docs/utilities/image.mdx:
--------------------------------------------------------------------------------
1 | # Image
2 |
3 | The Image component provides automatic fallback handling for broken images. Use this for robust media display with graceful degradation.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { Image } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UseImageDemo } from '../../demo/ImageDemo.tsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { Image } from 'react-haiku';
19 |
20 | export const Component = () => {
21 | return (
22 |
28 | );
29 | };
30 | ```
31 |
32 | ### API
33 |
34 | This component extends img HTML attributes and adds:
35 |
36 | - `fallback` - Required string for fallback image URL
37 | - All standard img props (src, alt, loading, etc.)
38 |
--------------------------------------------------------------------------------
/docs/docs/hooks/useScript.mdx:
--------------------------------------------------------------------------------
1 | # useScript()
2 |
3 | The `useScript()` hook allows appending script tags to your document from inside React components. The state variable returns a status that can have one of the following values: `idle`, `loading`, `ready`, `error`.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useScript } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UseScriptDemo } from '../../demo/UseScriptDemo.jsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { useScript } from 'react-haiku';
19 |
20 | export const Component = () => {
21 | // GSAP
22 | const script = useScript('https://cdnjs.cloudflare.com/ajax/libs/gsap/3.10.4/gsap.min.js');
23 |
24 | return (
25 | <>
26 | Check the bottom of the body tag!
27 |
28 | {`Script Status: ${script}`}
29 |
30 | >
31 | );
32 | }
33 | ```
--------------------------------------------------------------------------------
/lib/hooks/useEventListener.ts:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react';
2 | import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';
3 |
4 | export function useEventListener(
5 | eventName: string,
6 | handler: (e: Event) => any,
7 | element?: React.RefObject,
8 | ) {
9 | const savedHandler = useRef(handler);
10 |
11 | useIsomorphicLayoutEffect(() => {
12 | savedHandler.current = handler;
13 | }, [handler]);
14 |
15 | useEffect(() => {
16 | const targetElement = element?.current || window;
17 |
18 | if (!(targetElement && targetElement.addEventListener)) {
19 | return;
20 | }
21 |
22 | const eventListener = (event: Event) => savedHandler.current(event);
23 | targetElement.addEventListener(eventName, eventListener);
24 |
25 | return () => {
26 | targetElement.removeEventListener(eventName, eventListener);
27 | };
28 | }, [eventName, element]);
29 | }
30 |
--------------------------------------------------------------------------------
/lib/hooks/useScrollPosition.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { useEventListener } from './useEventListener';
3 |
4 | const getPosition = () =>
5 | typeof window !== 'undefined'
6 | ? { x: window.scrollX, y: window.scrollY }
7 | : { x: 0, y: 0 };
8 |
9 | const setPosition = ({ x, y }: { x?: number; y?: number }) => {
10 | if (typeof window !== 'undefined') {
11 | const scrollOptions: ScrollToOptions = { behavior: 'smooth' };
12 |
13 | if (typeof x === 'number') scrollOptions.left = x;
14 | if (typeof y === 'number') scrollOptions.top = y;
15 |
16 | window.scrollTo(scrollOptions);
17 | }
18 | };
19 |
20 | export function useScrollPosition() {
21 | const [currentPosition, setCurrentPosition] = useState(getPosition());
22 |
23 | ['scroll', 'resize'].forEach((item) =>
24 | useEventListener(item, () => setCurrentPosition(getPosition())),
25 | );
26 |
27 | return [currentPosition, setPosition];
28 | }
29 |
--------------------------------------------------------------------------------
/docs/docs/hooks/useBatteryStatus.mdx:
--------------------------------------------------------------------------------
1 | # useBatteryStatus()
2 |
3 | The `useBatteryStatus()` hook provides real-time information about the device's battery level and charging status, automatically updating as these values change.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useBatteryStatus } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import BrowserOnly from '@docusaurus/BrowserOnly';
14 | import { UseBatteryStatusDemo } from '../../demo/UseBatteryStatusDemo.jsx';
15 |
16 | Loading...}>
17 | {() => }
18 |
19 |
20 | ```jsx
21 |
22 | import { useBatteryStatus } from 'react-haiku';
23 |
24 | export const Component = () => {
25 | const {level, isCharging} = useBatteryStatus();
26 |
27 | return (
28 |
29 |
Battery Level: {level}
30 |
Is Battery Charging: {isCharging ? "True" : "False"}
31 |
32 | );
33 | }
34 |
35 | ```
--------------------------------------------------------------------------------
/docs/docs/hooks/usePreventBodyScroll.mdx:
--------------------------------------------------------------------------------
1 | # usePreventBodyScroll()
2 |
3 | The `usePreventBodyScroll()` hook disables body scrolling when active and restores it upon deactivation or component unmounting. It provides a boolean state, a setter, and a toggle function for dynamic scroll control.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { usePreventBodyScroll } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UsePreventBodyScrollDemo } from '../../demo/UsePreventBodyScrollDemo.jsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { usePreventBodyScroll } from 'react-haiku';
19 |
20 | export const Component = () => {
21 | const { isScrollLocked, toggleScrollLock } = usePreventBodyScroll();
22 |
23 | return (
24 | <>
25 | Scroll: {isScrollLocked ? 'Disabled' : 'Enabled'}
26 | toggleScrollLock()}>Toggle Scroll!
27 | >
28 | );
29 | }
30 | ```
--------------------------------------------------------------------------------
/docs/docs/hooks/useIsomorphicLayoutEffect.mdx:
--------------------------------------------------------------------------------
1 | # useIsomorphicLayoutEffect()
2 |
3 | The `useIsomorphicLayoutEffect()` hook lets switch between using `useEffect` and `useLayoutEffect` depending on the execution environment. If your app uses server side rendering, the hook will run `useEffect`, otherwise it will run `useLayoutEffect`.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useIsomorphicLayoutEffect } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UseIsomorphicLayoutEffectDemo } from '../../demo/UseIsomorphicLayoutEffectDemo.jsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { useIsomorphicLayoutEffect } from "react-haiku"
19 |
20 | export const Component = () => {
21 | useIsomorphicLayoutEffect(() => {
22 | // do whatever
23 | }, [])
24 |
25 | return (
26 | <>
27 | SSR will run useEffect
28 | Browser will run useLayoutEffect
29 | >
30 | );
31 | }
32 | ```
33 |
--------------------------------------------------------------------------------
/lib/utils/Switch.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentType, FC, ReactNode } from "react";
2 |
3 | type CaseComponent = ReactNode | ComponentType;
4 |
5 | export interface SwitchProps {
6 | /** List of case components to be rendered when case matches */
7 | components: Record;
8 | /** Default component to be rendered if the value does not match any of the given cases */
9 | defaultComponent: CaseComponent;
10 | /** Value to check with the cases to render the corresponding component */
11 | value: string | number;
12 | }
13 |
14 | /**
15 | * A component that renders one of the provided case components based on the value.
16 | */
17 | export const Switch: FC = ({ components, defaultComponent, value }) => {
18 | let RenderedComponent = (components[value] || defaultComponent) as CaseComponent;
19 |
20 | if (typeof RenderedComponent === "function") {
21 | return ;
22 | }
23 |
24 | return <>{RenderedComponent}>;
25 | };
--------------------------------------------------------------------------------
/lib/hooks/useConfirmExit.ts:
--------------------------------------------------------------------------------
1 | import { off, on } from '../helpers/event';
2 | import { useCallback, useEffect } from 'react';
3 |
4 | export function useConfirmExit(
5 | enabled: boolean | (() => boolean),
6 | message = 'Are you sure you want to exit?',
7 | ) {
8 | const handler = useCallback(
9 | (e: Event) => {
10 | const finalEnabled = typeof enabled === 'function' ? enabled() : true;
11 |
12 | if (!finalEnabled) {
13 | return;
14 | }
15 |
16 | e.preventDefault();
17 |
18 | // NOTE: modern browsers no longer support custom messages with .returnValue
19 | if (message) {
20 | // @ts-ignore
21 | e.returnValue = message;
22 | }
23 |
24 | return message;
25 | },
26 | [enabled, message],
27 | );
28 |
29 | useEffect(() => {
30 | if (!enabled) {
31 | return;
32 | }
33 |
34 | on(window, 'beforeunload', handler);
35 |
36 | return () => off(window, 'beforeunload', handler);
37 | }, [enabled, handler]);
38 | }
39 |
--------------------------------------------------------------------------------
/docs/demo/UseTimerDemo.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useTimer } from 'react-haiku';
3 |
4 | export const UseTimerDemo = () => {
5 | const { time, isRunning, start, pause, reset } = useTimer({
6 | startTime: 0,
7 | endTime: 10000,
8 | interval: 1000,
9 | });
10 |
11 | return (
12 |
13 |
Time: {time}
14 |
15 |
20 | Start
21 |
22 |
23 |
28 | Pause
29 |
30 |
31 | Reset
32 |
33 |
34 |
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/lib/hooks/useCookie.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | import { useCookieListener } from './useCookieListener';
4 | import { useSingleEffect } from './useSingleEffect';
5 |
6 | import {
7 | getCookie,
8 | setCookie,
9 | deleteCookie,
10 | } from '../helpers/cookie';
11 |
12 | export const useCookie = (
13 | key: string,
14 | initialValue: T,
15 | expireDays = 365,
16 | ) => {
17 | const [cookieValue, setCookieValue] = useState(
18 | getCookie(key) ?? initialValue,
19 | );
20 |
21 | useSingleEffect(() => {
22 | if (typeof getCookie(key) === "undefined") {
23 | setCookie(key, initialValue, expireDays);
24 | }
25 | });
26 |
27 | useCookieListener(
28 | (value: T) => {
29 | setCookieValue(value);
30 | },
31 | [key],
32 | );
33 |
34 | const setValue = (value: T) => {
35 | setCookieValue(value);
36 | setCookie(key, value, expireDays);
37 | };
38 |
39 | const deleteValue = () => {
40 | deleteCookie(key);
41 | };
42 |
43 | return [cookieValue, setValue, deleteValue];
44 | };
45 |
--------------------------------------------------------------------------------
/lib/hooks/usePreventBodyScroll.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | enum ScrollState {
4 | SCROLL = '',
5 | LOCK = 'hidden',
6 | }
7 |
8 | type PreventBodyScrollResult = {
9 | isScrollLocked: boolean;
10 | setIsScrollLocked: React.Dispatch>;
11 | toggleScrollLock: () => void;
12 | };
13 |
14 | export function usePreventBodyScroll(): PreventBodyScrollResult {
15 | const [isScrollLocked, setIsScrollLocked] = useState(false);
16 |
17 | useEffect(() => {
18 | if (isScrollLocked) {
19 | document.body.style.overflow = ScrollState.LOCK;
20 | } else {
21 | document.body.style.overflow = ScrollState.SCROLL;
22 | }
23 |
24 | return () => {
25 | document.body.style.overflow = ScrollState.SCROLL;
26 | };
27 | }, [isScrollLocked]);
28 |
29 | const toggleScrollLock = () => setIsScrollLocked((prevState) => !prevState);
30 |
31 | return {
32 | isScrollLocked,
33 | setIsScrollLocked,
34 | toggleScrollLock,
35 | };
36 | }
37 |
--------------------------------------------------------------------------------
/docs/demo/UseEventListenerDemo.jsx:
--------------------------------------------------------------------------------
1 |
2 | import { useEventListener } from "react-haiku";
3 | import React from 'react';
4 |
5 | export const UseEventListenerDemo = () => {
6 | const [countWindow, setCountWindow] = React.useState(0);
7 | const [countRef, setCountRef] = React.useState(0);
8 |
9 | // Button Ref
10 | const buttonRef = React.useRef(null)
11 |
12 | // Event Handlers
13 | const countW = () => setCountWindow(countWindow + 1);
14 | const countR = () => setCountRef(countRef + 1);
15 |
16 | // Example 1: Window Event
17 | useEventListener('scroll', countW);
18 |
19 | // Example 2: Element Event
20 | useEventListener('click', countR, buttonRef);
21 |
22 | return (
23 |
24 | Window Event Triggered {countWindow} Times!
25 | Ref Event Triggered {countRef} Times!
26 | Click Me
27 |
28 | );
29 | }
--------------------------------------------------------------------------------
/lib/tests/hooks/usePrevious.test.tsx:
--------------------------------------------------------------------------------
1 | import { renderHook, act } from '@testing-library/react';
2 | import { usePrevious } from "../../hooks/usePrevious";
3 |
4 | describe('usePrevious Hook', () => {
5 | test('returns the initial value initially', () => {
6 | const initialValue = "initial"
7 |
8 | const { result } = renderHook(() => usePrevious(initialValue));
9 | expect(result.current).toBe(initialValue);
10 | });
11 |
12 | test('returns the previous value', () => {
13 | const initialValue = "initial"
14 |
15 | const { result, rerender } = renderHook(({ value }) => usePrevious(value), {
16 | initialProps: {
17 | value: initialValue
18 | }
19 | });
20 |
21 | expect(result.current).toBe(initialValue)
22 |
23 | act(() => {
24 | rerender({ value: "previous" });
25 | });
26 |
27 | expect(result.current).toBe(initialValue)
28 |
29 | act(() => {
30 | rerender({ value: "next" });
31 | });
32 |
33 | expect(result.current).toBe("previous")
34 | })
35 | })
36 |
--------------------------------------------------------------------------------
/docs/docs/hooks/useSize.mdx:
--------------------------------------------------------------------------------
1 | # useSize()
2 |
3 | The `useSize` hook observes a referenced DOM element and returns its current width and height, updating the values whenever the element is resized. This is useful for dynamically tracking size changes of any resizable component.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useSize } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UseSizeDemo } from '../../demo/UseSizeDemo.jsx';
14 |
15 |
16 |
17 | ```jsx
18 |
19 | import { useRef } from "react"
20 | import { useSize } from "react-haiku"
21 |
22 | export const Component = () => {
23 | const elementRef = useRef(null);
24 | const { width, height } = useSize(elementRef);
25 |
26 | return (
27 |
28 |
Window Height: {height}
29 |
Window Width: {width}
30 |
31 | );
32 | }
33 |
34 | ```
35 |
36 | ### API
37 | This hook accepts the following arguments:
38 |
39 | - `ref` - a reference to the DOM element you want to observe for size changes.
--------------------------------------------------------------------------------
/lib/hooks/useDeviceOS.ts:
--------------------------------------------------------------------------------
1 | export const useDeviceOS = () => {
2 | // @ts-ignore
3 | const { platform } = navigator?.userAgentData || {};
4 |
5 | if (!!platform) {
6 | return platform;
7 | }
8 | // For mobile emulators on browsers
9 | return checkOSBasedOnAgentInfo(navigator.userAgent);
10 | };
11 |
12 | const checkOSBasedOnAgentInfo = (info: string) => {
13 | switch (true) {
14 | case info.includes('iPhone') || info.includes('iPad'):
15 | return 'iOS';
16 | case info.includes('Linux'):
17 | return 'Linux';
18 | case info.includes('Windows'):
19 | return 'Windows';
20 | default:
21 | return extractUniqueOS(info);
22 | }
23 | };
24 |
25 | const extractUniqueOS = (info: string) => {
26 | const regex = /\(([^)]+)\)/;
27 | const matches = info.match(regex);
28 |
29 | if (matches && matches.length > 1) {
30 | const deviceText = matches[1];
31 | const firstWord = deviceText?.trim().split(' ')[0];
32 | return firstWord?.slice(0, -1);
33 | }
34 |
35 | return 'Unknown';
36 | };
37 |
--------------------------------------------------------------------------------
/docs/demo/UsePreviousDemo.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useState } from "react";
3 | import { usePrevious } from "react-haiku"
4 |
5 | export const UsePreviousDemo = () => {
6 | const [count, setCount] = useState(0);
7 | const prevCount = usePrevious(count);
8 |
9 | function handleIncrement() {
10 | setCount(prev => prev + 1)
11 | }
12 |
13 | function handleDecrement() {
14 | setCount(prev => prev - 1)
15 | }
16 |
17 | return (
18 |
19 |
Current Value: {count}
20 |
Previous Value: {prevCount}
21 |
22 |
23 |
24 | Increment
25 |
26 |
27 | Decrement
28 |
29 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/docs/docs/hooks/useUpdateEffect.mdx:
--------------------------------------------------------------------------------
1 | # useUpdateEffect()
2 |
3 | The `useUpdateEffect()` hook will work exactly like a `useEffect()` hook, except it will skip the first render and only react to changes for values passed inside its dependency array after the initial render.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useUpdateEffect } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UseUpdateEffectDemo } from '../../demo/UseUpdateEffectDemo.jsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { useState } from 'react';
19 | import { useUpdateEffect } from 'react-haiku';
20 |
21 | export const Component = () => {
22 | const [count, setCount] = useState(0);
23 | const [triggerCount, setTriggerCount] = useState(0);
24 |
25 | useUpdateEffect(() => {
26 | setTriggerCount(triggerCount + 1);
27 | }, [count])
28 |
29 | return (
30 | <>
31 | Updates Detected: {triggerCount}
32 | setCount(count + 1)}>Update State
33 | >
34 | );
35 | }
36 | ```
--------------------------------------------------------------------------------
/docs/LICENSE.md:
--------------------------------------------------------------------------------
1 | React Haiku by David Haz
2 |
3 | Copyright 2025 David Haz
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | React Haiku by David Haz
2 |
3 | Copyright 2025 David Haz
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/docs/demo/UseSizeDemo.jsx:
--------------------------------------------------------------------------------
1 | import { useSize} from "react-haiku"
2 | import React from 'react';
3 |
4 | export const UseSizeDemo = () => {
5 | const elementRef = React.useRef(null);
6 | const { width, height } = useSize(elementRef);
7 | return (
8 |
9 |
Resize Me!
10 |
23 |
Width: {width} px
24 |
Height: {height} px
25 |
26 |
27 | );
28 | }
--------------------------------------------------------------------------------
/docs/demo/UsePermissionDemo.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useState } from "react";
3 | import { usePermission } from "react-haiku"
4 |
5 | export const UsePermissionDemo = () => {
6 | const state = usePermission("geolocation")
7 | const [location, setLocation] = useState(null)
8 |
9 | function handleGetCurrentPosition() {
10 | if (state !== "prompt" && state !== "granted") return
11 |
12 | navigator.geolocation.getCurrentPosition((location) => {
13 | setLocation(location)
14 | })
15 | }
16 |
17 | return (
18 |
19 |
Permission state: {state}
20 |
21 |
22 | {JSON.stringify(location ?? {}, null, 2)}
23 |
24 |
25 |
26 |
30 | Get current position
31 |
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/docs/docs/hooks/useScrollPosition.mdx:
--------------------------------------------------------------------------------
1 | # useScrollPosition()
2 |
3 | The `useScrollPosition()` hook allows you to fetch the window's scroll height/width in real time and to programatically set them by using the provided method.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useScrollPosition } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UseScrollPositionDemo } from '../../demo/UseScrollPositionDemo.jsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { useScrollPosition } from 'react-haiku';
19 |
20 | export const Component = () => {
21 | const [scroll, setScroll] = useScrollPosition();
22 |
23 | return (
24 | <>
25 | Current Position: {`X: ${scroll.x}, Y: ${scroll.y}`}!
26 | setScroll({ y: document.body.scrollHeight })}>
27 | Scroll To Bottom
28 |
29 | >
30 | );
31 | }
32 | ```
33 |
34 | ### API
35 |
36 | - `scroll` – contains the x and y values of the window's scroll position
37 | - `setScroll` - used for programatically scrolling inside the active viewport
--------------------------------------------------------------------------------
/docs/demo/ShowDemo.jsx:
--------------------------------------------------------------------------------
1 | import { Show } from "react-haiku"
2 | import React from 'react';
3 |
4 | export const ShowDemo = () => {
5 | const [number, setNumber] = React.useState(6);
6 |
7 | return (
8 |
9 |
10 |
11 | Number is 6!
12 | setNumber(number + 1)}>Increment
13 |
14 |
15 | Number is 7!
16 | setNumber(number + 1)}>Increment
17 |
18 |
19 | No valid number found!
20 | setNumber(6)}>Reset
21 |
22 |
23 |
24 | );
25 | }
--------------------------------------------------------------------------------
/lib/hooks/useClipboard.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | export function useClipboard({ timeout = 500 } = {}) {
4 | const [error, setError] = useState(null);
5 | const [copied, setCopied] = useState(false);
6 | const [copyTimeout, setCopyTimeout] = useState(undefined);
7 |
8 | const handleCopyResult = (hasError: boolean) => {
9 | clearTimeout(copyTimeout);
10 |
11 | setCopyTimeout(
12 | setTimeout(() => setCopied(false), timeout) as unknown as number,
13 | );
14 |
15 | setCopied(hasError);
16 | };
17 |
18 | const copy = (value: string) => {
19 | if ('clipboard' in navigator) {
20 | navigator.clipboard
21 | .writeText(value)
22 | .then(() => handleCopyResult(true))
23 | .catch((err) => setError(err));
24 | } else {
25 | setError(new Error('Error: navigator.clipboard is not supported'));
26 | }
27 | };
28 |
29 | const reset = () => {
30 | setError(null);
31 | setCopied(false);
32 | clearTimeout(copyTimeout);
33 | };
34 |
35 | return { copy, reset, error, copied };
36 | }
37 |
--------------------------------------------------------------------------------
/docs/docs/hooks/useClipboard.mdx:
--------------------------------------------------------------------------------
1 | # useClipboard()
2 |
3 | The useClipboard() hook will help you interact with the browser's navigator.clipboard property in order to copy text to the user's clipboard.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useClipboard } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UseClipboardDemo } from '../../demo/UseClipboardDemo.jsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { useClipboard } from 'react-haiku';
19 |
20 | const Component = () => {
21 | const clipboard = useClipboard({ timeout: 2000 });
22 |
23 | return (
24 | clipboard.copy('Haiku Rocks!')}>
25 | {clipboard.copied ? 'Copied' : 'Copy'}
26 |
27 | );
28 | }
29 | ```
30 |
31 | ### API
32 |
33 | The useClipboard hook takes an argument `options` where you can set the timeout duration for a successful copy operation
34 |
35 | - `copy` – method use to copy a string to the clipboard
36 | - `copied` – boolean value that indicates a successful copy
37 | - `reset` – method that can clear any timeout and reset the `copied` value
38 | - `error` – error handling object returned if any potential errors occur
--------------------------------------------------------------------------------
/docs/static/img/sponsor.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/docs/docs/hooks/useClickOutside.mdx:
--------------------------------------------------------------------------------
1 | # useClickOutside()
2 |
3 | The useClickOutside() hook lets you trigger a callback whenever the user clicks outside of a target element
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useClickOutside } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UseClickOutsideDemo } from '../../demo/UseClickOutsideDemo.jsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { useState, useRef } from 'react'
19 | import { useClickOutside } from "react-haiku"
20 |
21 | export const Component = () => {
22 | const [count, setCount] = useState(0);
23 | const ref = useRef(null)
24 |
25 | const handleClickOutside = () => setCount(count + 1);
26 |
27 | useClickOutside(ref, handleClickOutside);
28 |
29 | return (
30 | <>
31 | Clicked Outside {count} Times!
32 | Click Outside Of Me!
33 | >
34 | );
35 | }
36 | ```
37 |
38 | ### API
39 |
40 | This hook accepts the following arguments:
41 | - `ref` - the target element, clicking outside of this element will trigger the handler
42 | - `handler` - the handler for what happens when you click outside of the target
--------------------------------------------------------------------------------
/docs/docs/hooks/useLocalStorage.mdx:
--------------------------------------------------------------------------------
1 | # useLocalStorage()
2 |
3 | The `useLocalStorage()` hook is a quick way to set, read, and manage `localStorage` values. It comes with automatic JSON serialization/deserialization.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useLocalStorage } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UseLocalStorageDemo } from '../../demo/UseLocalStorageDemo.jsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { useEffect } from 'react'
19 | import { useLocalStorage } from "react-haiku"
20 |
21 | export const Component = () => {
22 | const [value, setValue] = useLocalStorage('message');
23 |
24 | useEffect(() => {
25 | setValue({ message: 'Hello!' })
26 | }, [])
27 |
28 | return (
29 | <>
30 | Storage Value: {value.message}
31 | setValue({ message: 'Woah!' })}>Update Storage
32 | >
33 | );
34 | }
35 | ```
36 |
37 | ### API
38 |
39 | This hook accetps the following arguments:
40 |
41 | - `key` - the key to use for a localStorage entry
42 | - `value` - the value corresponding to a certain key, can be a a simple string value or an object
--------------------------------------------------------------------------------
/lib/hooks/useMousePosition.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from 'react';
2 |
3 | export function useMousePosition() {
4 | const [position, setPosition] = useState({ x: 0, y: 0 });
5 | const { max } = Math;
6 | const target = useRef();
7 |
8 | const setMousePosition = (event: MouseEvent) => {
9 | if (target.current) {
10 | // @ts-ignore
11 | const r = event?.currentTarget?.getBoundingClientRect();
12 | const x = max(
13 | 0,
14 | Math.round(
15 | event.pageX - r.left - (window.pageXOffset || window.scrollX),
16 | ),
17 | );
18 | const y = max(
19 | 0,
20 | Math.round(
21 | event.pageY - r.top - (window.pageYOffset || window.scrollY),
22 | ),
23 | );
24 | setPosition({ x, y });
25 | } else setPosition({ x: event.clientX, y: event.clientY });
26 | };
27 |
28 | useEffect(() => {
29 | const element = target?.current ? target.current : document;
30 | element.addEventListener('mousemove', setMousePosition);
31 |
32 | return () => element.removeEventListener('mousemove', setMousePosition);
33 | }, [target.current]);
34 |
35 | return { target, ...position };
36 | }
37 |
--------------------------------------------------------------------------------
/docs/docs/hooks/useOrientation.mdx:
--------------------------------------------------------------------------------
1 | # useOrientation()
2 |
3 | The useOrientation() hook detects and tracks device screen orientation changes. Use this when you need to adapt your UI layout based on portrait/landscape modes.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useOrientation } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import BrowserOnly from '@docusaurus/BrowserOnly';
14 | import { UseOrientationDemo } from '../../demo/UseOrientationDemo.tsx';
15 |
16 | Loading...}>
17 | {() => }
18 |
19 |
20 | ```jsx
21 | import { useOrientation } from 'react-haiku';
22 |
23 | export const Component = () => {
24 | const orientation = useOrientation();
25 |
26 | return (
27 | <>
28 | Current Orientation:
29 |
32 | {orientation.toUpperCase()}
33 |
34 | >
35 | );
36 | };
37 | ```
38 |
39 | ### API
40 |
41 | #### Arguments
42 |
43 | - This hook requires no arguments
44 |
45 | #### Returns
46 |
47 | - orientation - A string type union of either "portrait" or "landscape"
48 |
--------------------------------------------------------------------------------
/docs/docs/utilities/if.mdx:
--------------------------------------------------------------------------------
1 | # If
2 |
3 | The `If` component can be used for simple conditional rendering. It will render its children whenever the condition passed to the `isTrue` prop is truthy. For more complex rendering logic, you can use the `Show` component
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { If } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { IfDemo } from '../../demo/IfDemo.jsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { useState } from 'react';
19 | import { If } from 'react-haiku';
20 |
21 | export const Component = () => {
22 | const [number, setNumber] = useState(6);
23 |
24 | return(
25 | <>
26 | Click to update state!
27 |
28 | setNumber(7)}>I like the number 6!
29 |
30 |
31 | setNumber(6)}>7 is way better!
32 |
33 | >
34 | );
35 | }
36 | ```
37 |
38 | ### API
39 |
40 | The `If` component accepts the following props:
41 |
42 | - `isTrue` - the condition to evaluate when rendering the component's contents
--------------------------------------------------------------------------------
/docs/docs/hooks/useMediaQuery.mdx:
--------------------------------------------------------------------------------
1 | # useMediaQuery()
2 |
3 | The `useMediaQuery()` hook allows you to react to media queries inside of your React components. It accepts a `media query` argument and returns `true` or `false` when the query is a match or not with your current browser's properties.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useMediaQuery } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UseMediaQueryDemo } from '../../demo/UseMediaQueryDemo.jsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { useMediaQuery } from 'react-haiku';
19 |
20 | export const Component = () => {
21 | const breakpoint = useMediaQuery('(max-width: 1200px)');
22 |
23 | return (
24 | <>
25 | Resize your window!
26 |
27 | {breakpoint ? "It's a match for 1200px!" : "Not a match for 1200px!"}
28 |
29 | >
30 | );
31 | }
32 | ```
33 |
34 | ### API
35 |
36 | - `initialValue` – Boolean value to specify the initial value of the query matches result
37 |
38 | ```jsx
39 | const breakpoint = useMediaQuery('(max-width: 1000px)', false);
40 | ```
--------------------------------------------------------------------------------
/docs/docs/hooks/usePrevious.mdx:
--------------------------------------------------------------------------------
1 | # usePrevious()
2 |
3 | The `usePrevious()` tracks and returns the previous value of a given input
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { usePrevious } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UsePreviousDemo } from '../../demo/UsePreviousDemo.tsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { usePrevious } from 'react-haiku';
19 |
20 | export const Component = () => {
21 | const [count, setCount] = useState(0);
22 | const prevCount = usePrevious(count);
23 |
24 | function handleIncrement() {
25 | setCount(prev => prev + 1)
26 | }
27 |
28 | function handleDecrement() {
29 | setCount(prev => prev - 1)
30 | }
31 |
32 | return (
33 |
34 |
Current Value: {count}
35 |
Previous Value: {prevCount}
36 |
37 |
38 |
39 | Increment
40 |
41 |
42 | Decrement
43 |
44 |
45 |
46 | );
47 | }
48 | ```
49 |
50 | ### API
51 |
52 | #### Arguments
53 |
54 | - `value` - The new value to track and return the previous of
55 |
--------------------------------------------------------------------------------
/docs/docs/hooks/useConfirmExit.mdx:
--------------------------------------------------------------------------------
1 | # useConfirmExit()
2 |
3 | The useConfirmExit() hook lets you display a prompt to the user before he closes the current tab depending on whether the tab is declared to be dirty or not.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useConfirmExit } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UseConfirmExitDemo } from '../../demo/UseConfirmExitDemo.jsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { useConfirmExit, useBoolToggle } from "react-haiku"
19 |
20 | export const Component = () => {
21 | const [dirty, toggleDirty] = useBoolToggle();
22 |
23 | useConfirmExit(dirty);
24 |
25 | return (
26 | <>
27 | Try to close this tab with the window dirty!
28 | Dirty: {`${dirty}`}
29 | toggleDirty()}>{dirty ? 'Set Clean' : 'Set Dirty'}
30 | >
31 | );
32 | }
33 | ```
34 |
35 | ### API
36 |
37 | This hook accepts the following arguments:
38 | - `dirty` - the value to check when activating/deactivating the exit confirmation message
39 | - `message` - while this is availabe, using it currently has no effect since most browsers have disabled the feature of adding custom confirmation messages on this type of event
--------------------------------------------------------------------------------
/lib/hooks/useIdle.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef } from 'react';
2 |
3 | const defaultEvents = [
4 | 'keypress',
5 | 'mousemove',
6 | 'touchmove',
7 | 'click',
8 | 'scroll',
9 | ] as const;
10 | type DefaultEvent = (typeof defaultEvents)[number];
11 | type Options = {
12 | events?: DefaultEvent[];
13 | initialState?: boolean;
14 | };
15 | const defaultOptions: Options = {
16 | events: [...defaultEvents],
17 | initialState: true,
18 | };
19 |
20 | export function useIdle(timeout: number, options: Options = {}) {
21 | const { events, initialState }: Options = { ...defaultOptions, ...options };
22 | const [idle, setIdle] = useState(initialState);
23 | const timer = useRef(undefined);
24 |
25 | useEffect(() => {
26 | const handleEvents = () => {
27 | setIdle(false);
28 |
29 | if (timer.current) {
30 | window.clearTimeout(timer.current);
31 | }
32 |
33 | timer.current = window.setTimeout(() => setIdle(true), timeout);
34 | };
35 |
36 | events?.forEach((event) => document.addEventListener(event, handleEvents));
37 |
38 | return () =>
39 | events?.forEach((event) =>
40 | document.removeEventListener(event, handleEvents),
41 | );
42 | }, [timeout]);
43 |
44 | return idle;
45 | }
46 |
--------------------------------------------------------------------------------
/docs/docs/hooks/useKeyPress.mdx:
--------------------------------------------------------------------------------
1 | # useKeyPress()
2 |
3 | The `useKeyPress()` hook listens for a specific combination of keys and runs a callback when they are all pressed. It normalizes keys for case-insensitive matching and handles cases like key holding or focus loss to ensure smooth behavior.
4 |
5 | ### Import
6 |
7 |
8 | ```jsx
9 | import { useKeyPress } from 'react-haiku';
10 | ```
11 |
12 | ### Usage
13 |
14 | import { UseKeyPressDemo } from '../../demo/UseKeyPressDemo.jsx';
15 |
16 |
17 |
18 | ```jsx
19 | import { useKeyPress } from 'react-haiku';
20 |
21 | const Component = () => {
22 | const [didKeyPress, setDidKeyPress] = useState(false);
23 | useKeyPress(['Control', 'Shift', 'A'], (e) => {
24 | setDidKeyPress(true);
25 | });
26 |
27 | return (
28 |
29 |
Press Control + Shift + A
30 | {didKeyPress &&
{`You pressed : Control + Shift + A`}
}
31 |
32 | );
33 | };
34 |
35 | export default App;
36 | ```
37 |
38 | ### API
39 | This hook accepts the following arguments:
40 | - `keys` - An array of key names (case-insensitive) that should be pressed to trigger the callback
41 | - `callback` - The function to be executed when specified keys are pressed.
42 |
43 | ### Returns
44 | - `void` - This hook does not return anything.
--------------------------------------------------------------------------------
/docs/demo/ErrorBoundaryDemo.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ErrorBoundary } from 'react-haiku';
3 |
4 | interface FallbackProps {
5 | retry: () => void;
6 | error: Error | null;
7 | errorInfo: React.ErrorInfo | null;
8 | }
9 |
10 | const Fallback: React.FC = ({ retry }) => {
11 | return (
12 |
13 | We faced an error
14 |
19 | Retry
20 |
21 |
22 | );
23 | };
24 |
25 | // Component that will intentionally cause an error
26 | const CrashComponent: React.FC = () => {
27 | // Only throw an error if window is defined (i.e., in a browser environment)
28 | // This prevents the error during Docusaurus build (SSR)
29 | if (typeof window !== 'undefined') {
30 | throw new Error('This is a test error inside the ErrorBoundary!');
31 | }
32 | return This component would normally throw an error to demonstrate the ErrorBoundary.
;
33 | };
34 |
35 | export const ErrorBoundaryDemo: React.FC = () => {
36 | return (
37 |
38 |
39 |
40 | );
41 | };
42 |
--------------------------------------------------------------------------------
/lib/utils/Class.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, ElementType } from "react";
2 | interface ClassProps {
3 | className?: string;
4 | condition?: boolean;
5 | toggleClass?: string;
6 | children: ReactNode;
7 | as?: ElementType;
8 | [key: string]: any;
9 | }
10 |
11 | /**
12 | * Class component that conditionally applies a class name to a specified HTML element.
13 | *
14 | * @param className - The initial class name for the element.
15 | * @param condition - The condition to determine whether to apply the toggle class or not.
16 | * @param toggleClass - The class name to be toggled based on the condition.
17 | * @param children - The content to be rendered inside the specified HTML element.
18 | * @param as - The type of HTML element to render.
19 | * @param props - Any additional props to be passed to the element.
20 | * @returns The rendered Class component.
21 | */
22 | export const Class: React.FC = ({
23 | className = "",
24 | condition = false,
25 | toggleClass = "",
26 | children,
27 | as: Component = "div",
28 | ...props
29 | }) => {
30 | const computedClassName = condition
31 | ? `${className} ${toggleClass}`.trim()
32 | : className;
33 |
34 | return (
35 |
36 | {children}
37 |
38 | );
39 | };
--------------------------------------------------------------------------------
/docs/docs/hooks/useNetwork.mdx:
--------------------------------------------------------------------------------
1 | # useNetwork()
2 |
3 | The useNetwork() hook tracks network connectivity status. Use this to show offline/online indicators or handle connection changes in your application.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useNetwork } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import BrowserOnly from '@docusaurus/BrowserOnly';
14 | import { UseNetworkDemo } from '../../demo/UseNetworkDemo.tsx';
15 |
16 | Loading...}>
17 | {() => }
18 |
19 |
20 | ```jsx
21 | import { useNetwork } from 'react-haiku';
22 |
23 | export const Component = () => {
24 | const isOnline = useNetwork();
25 |
26 | return (
27 |
28 |
Network Status:
29 |
30 | {isOnline ? 'ONLINE' : 'OFFLINE'}
31 |
32 |
33 | Toggle network status in Chrome DevTools (Application → Service Workers
34 | → Offline)
35 |
36 |
37 | );
38 | };
39 | ```
40 |
41 | ### API
42 |
43 | #### Arguments
44 |
45 | - This hook requires no arguments
46 |
47 | #### Returns
48 |
49 | - `isOnline` - Boolean indicating network connectivity status (true = online, false = offline)
50 |
--------------------------------------------------------------------------------
/docs/docs/hooks/useScreenSize.mdx:
--------------------------------------------------------------------------------
1 | # useScreenSize()
2 |
3 | The `useScreenSize()` hook allows responsive breakpoint detection in components. It returns helper methods (`equals`, `lessThan`, `greaterThan` , `greaterThanEqual` , `lessThanEqual` ) and a string method (`toString`) representing the current screen size: `xs`, `sm`, `md`, `lg`, `xl`, or `2xl for size bigger than 1535 pixel`.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useScreenSize } from 'react-haiku';
9 | ```
10 |
11 |
12 | ### Usage
13 |
14 | import { UseScreenSizeDemo } from '../../demo/UseScreenSizeDemo.jsx';
15 |
16 |
17 |
18 | ```jsx
19 | import { useScreenSize } from 'react-haiku';
20 |
21 | export const MyComponent = () => {
22 | const screenSize = useScreenSize();
23 |
24 | return (
25 |
26 |
Current Screen Size: {screenSize.toString()}
27 |
Is the screen size medium ? {screenSize.eq("md") ? "Yes" : "No"}
28 |
Is the screen size less than large ? {screenSize.lt("lg") ? "Yes" : "No"}
29 |
Is the screen size greater than small ? {screenSize.gt("sm") ? "Yes" : "No"}
30 |
Is the screen size greater than or equal to small ? {screenSize.gte("sm") ? "Yes" : "No"}
31 |
Is the screen size less than or equal to small ? {screenSize.lte("sm") ? "Yes" : "No"}
32 |
33 | );
34 | };
35 | ```
--------------------------------------------------------------------------------
/docs/docs/hooks/useHold.mdx:
--------------------------------------------------------------------------------
1 | # useHold()
2 |
3 | The `useHold()` hook lets you detect long presses (holds) on target elements and trigger a handler after a set timeout is elapsed while the user is still holding down (click/touch) the element.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useHold } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UseHoldDemo } from '../../demo/UseHoldDemo.jsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { useState } from 'react'
19 | import { useHold } from "react-haiku"
20 |
21 | export const Component = () => {
22 | const [count, setCount] = useState(0)
23 | const handleHold = () => setCount(count + 1);
24 |
25 | const buttonHold = useHold(handleHold, { delay: 2000 });
26 |
27 | return (
28 | <>
29 | Successful Holds: {count}
30 |
31 | Press & Hold!
32 |
33 | >
34 | );
35 | }
36 | ```
37 |
38 | ### API
39 |
40 | This hook accepts the following arguments:
41 |
42 | - `callback` - the handler function to execute on successful holds
43 | - `options` - the options object you can pass in
44 | - - `doPreventDefault` - boolean, whether or not to prevent the default event
45 | - - `delay` - number, a value in milliseconds, the amount of time the user needs to hold an element to trigger an event
--------------------------------------------------------------------------------
/docs/docs/hooks/useMousePosition.mdx:
--------------------------------------------------------------------------------
1 | # useMousePosition()
2 |
3 | The `useMousePosition()` hook lets you track the mouse position when hovering over a specific container or the entire page, so if a target container is not provided through the `ref`, it will track the mouse position relative to the entire document.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useMousePosition } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UseMousePositionDemo } from '../../demo/UseMousePositionDemo.jsx';
14 |
15 | #### Targeted Container
16 |
17 |
18 |
19 | ```jsx
20 | import { useMousePosition } from 'react-haiku';
21 |
22 | export const Component = () => {
23 | const { target, x, y } = useMousePosition();
24 |
25 | return (
26 |
27 |
Hover This Container
28 |
{`X: ${x} | Y: ${y}`}
29 |
30 | );
31 | }
32 | ```
33 |
34 | #### Entire Document
35 |
36 | import { UseMousePositionFullDemo } from '../../demo/UseMousePositionDemo.jsx';
37 |
38 |
39 |
40 | ```jsx
41 | import { useMousePosition } from 'react-haiku';
42 |
43 | export const Component = () => {
44 | const { x, y } = useMousePosition();
45 |
46 | return (
47 |
48 |
{`X: ${x} | Y: ${y}`}
49 |
50 | );
51 | }
52 | ```
53 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "haiku-docs",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "docusaurus": "docusaurus",
7 | "start": "docusaurus start",
8 | "build": "docusaurus build",
9 | "swizzle": "docusaurus swizzle",
10 | "deploy": "docusaurus deploy",
11 | "clear": "docusaurus clear",
12 | "serve": "docusaurus serve",
13 | "write-translations": "docusaurus write-translations",
14 | "write-heading-ids": "docusaurus write-heading-ids"
15 | },
16 | "dependencies": {
17 | "@docusaurus/core": "2.0.0-beta.20",
18 | "@docusaurus/plugin-google-analytics": "^2.0.0-beta.20",
19 | "@docusaurus/preset-classic": "2.0.0-beta.20",
20 | "@mdx-js/react": "^1.6.22",
21 | "@react-spring/web": "^9.7.4",
22 | "clsx": "^1.1.1",
23 | "prism-react-renderer": "^1.3.1",
24 | "react": "^17.0.2",
25 | "react-dom": "^17.0.2",
26 | "react-haiku": "2.4.1",
27 | "react-hot-toast": "^2.4.1",
28 | "swiper": "^11.1.9"
29 | },
30 | "devDependencies": {
31 | "@docusaurus/module-type-aliases": "2.0.0-beta.20"
32 | },
33 | "browserslist": {
34 | "production": [
35 | ">0.5%",
36 | "not dead",
37 | "not op_mini all"
38 | ],
39 | "development": [
40 | "last 1 chrome version",
41 | "last 1 firefox version",
42 | "last 1 safari version"
43 | ]
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/docs/src/components/Animations/AnimatedContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useSpring, animated } from '@react-spring/web';
3 |
4 | export const AnimatedContainer = ({ children, direction, reverse = false, endPosition = '0', skipObserver = false }) => {
5 | const [inView, setInView] = React.useState(false);
6 | const ref = React.useRef();
7 |
8 | React.useEffect(() => {
9 | const observer = new IntersectionObserver(
10 | ([entry]) => {
11 | if (entry.isIntersecting) {
12 | setInView(true);
13 | observer.unobserve(ref.current); // Unobserve after triggering the animation
14 | }
15 | },
16 | { threshold: 0.1 }
17 | );
18 |
19 | observer.observe(ref.current);
20 |
21 | return () => observer.disconnect();
22 | }, []);
23 |
24 | const directions = {
25 | vertical: "Y",
26 | horizontal: "X"
27 | }
28 |
29 | const springProps = useSpring({
30 | from: { transform: `translate${directions[direction]}(${reverse ? '-100px' : '100px'})` },
31 | to: inView || skipObserver ? { transform: `translate${directions[direction]}(${endPosition}px)` } : `translate${directions[direction]}(${reverse && '-'}100px)`,
32 | config: { tension: 50, friction: 25 },
33 | });
34 |
35 | return (
36 |
37 | {children}
38 |
39 | );
40 | };
--------------------------------------------------------------------------------
/docs/docs/hooks/useDebounce.mdx:
--------------------------------------------------------------------------------
1 | # useDebounce()
2 |
3 | The useDebounce() hook lets you debounce value changes inside your components. Use this when you want to perform a heavy operation based on state
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useDebounce } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UseDebounceDemo } from '../../demo/UseDebounceDemo.jsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { useState, useEffect } from 'react'
19 | import { useDebounce } from "react-haiku"
20 |
21 | export const Component = () => {
22 | const [value, setValue] = useState('')
23 | const debouncedValue = useDebounce(value, 1000)
24 |
25 | const handleChange = (event) => setValue(event.target.value)
26 |
27 | // Handle Change After Debounce
28 | useEffect(() => {
29 | console.log(debouncedValue);
30 | }, [debouncedValue])
31 |
32 | return (
33 | <>
34 | Real-Time Value: {value}
35 | Debounced value: {debouncedValue}
36 |
37 |
38 | >
39 | );
40 | }
41 | ```
42 |
43 | ### API
44 |
45 | This hook accepts the following arguments:
46 | - `value` - the state value that you want to debounce on changes
47 | - `timeout` - the amount of time in milliseconds to wait until the debounce is triggered, `500ms` by default
--------------------------------------------------------------------------------
/lib/tests/utils/Switch.test.tsx:
--------------------------------------------------------------------------------
1 | import { render } from '@testing-library/react';
2 | import { Switch } from '../../utils/Switch';
3 |
4 | const ComponentA = () => Component A
;
5 | const ComponentB = () => Component B
;
6 | const DefaultComponent = () => Default Component
;
7 |
8 | enum TestCases {
9 | A = 'a',
10 | B = 'b',
11 | DEFAULT = 'default'
12 | }
13 |
14 | const renderSwitch = (value: TestCases) => {
15 | return render(
16 |
24 | );
25 | };
26 |
27 | describe('Switch Component', () => {
28 | it('should render the correct component based on value', () => {
29 | const { getByText, asFragment } = renderSwitch(TestCases.A);
30 | expect(getByText('Component A')).toBeInTheDocument();
31 | expect(asFragment()).toMatchSnapshot();
32 | });
33 |
34 | it('should render the default component if value does not match any case', () => {
35 | const { getByText, asFragment } = renderSwitch(TestCases.DEFAULT);
36 | expect(getByText('Default Component')).toBeInTheDocument();
37 | expect(asFragment()).toMatchSnapshot();
38 | });
39 | });
--------------------------------------------------------------------------------
/lib/hooks/useMediaQuery.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef } from 'react';
2 |
3 | function attachMediaListener(
4 | query: any,
5 | callback: (event: MediaQueryListEvent) => any,
6 | ) {
7 | try {
8 | query.addEventListener('change', callback);
9 |
10 | return () => query.removeEventListener('change', callback);
11 | } catch (e) {
12 | query.addListener(callback);
13 |
14 | return () => query.removeListener(callback);
15 | }
16 | }
17 |
18 | function computeInitialValue(query: string, initialValue: T) {
19 | if (initialValue !== undefined) {
20 | return initialValue;
21 | }
22 |
23 | if (typeof window !== 'undefined' && 'matchMedia' in window) {
24 | return window.matchMedia(query).matches;
25 | }
26 |
27 | return false;
28 | }
29 |
30 | export function useMediaQuery(query: string, initialValue: T) {
31 | const [matches, setMatches] = useState(
32 | computeInitialValue(query, initialValue),
33 | );
34 | const ref = useRef();
35 |
36 | useEffect(() => {
37 | if ('matchMedia' in window) {
38 | ref.current = window.matchMedia(query);
39 | setMatches(ref.current.matches);
40 | return attachMediaListener(ref.current, (event: MediaQueryListEvent) =>
41 | setMatches(event.matches),
42 | );
43 | }
44 |
45 | return undefined;
46 | }, [query]);
47 |
48 | return matches;
49 | }
50 |
--------------------------------------------------------------------------------
/docs/demo/UseIntervalDemo.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { useInterval } from 'react-haiku';
3 | import React from 'react';
4 |
5 | export const UseIntervalDemo = () => {
6 | const [count, setCount] = useState(0);
7 | const { start, stop } = useInterval(() => {
8 | setCount((c) => c + 1);
9 | }, 1000);
10 |
11 | const handleRestart = () => {
12 | stop();
13 | setTimeout(() => {
14 | setCount(0);
15 | start(1000);
16 | }, 50);
17 | };
18 |
19 | return (
20 |
21 |
Interval Counter:
22 |
26 | {count}
27 |
28 |
29 |
30 |
31 | Stop
32 |
33 |
34 | Restart
35 |
36 |
37 |
38 |
46 | Restart will reset counter and interval
47 |
48 |
49 | );
50 | };
51 |
--------------------------------------------------------------------------------
/docs/docs/hooks/useCookieListener.mdx:
--------------------------------------------------------------------------------
1 | # useCookieListener()
2 |
3 | The useCookieListener() hook monitors cookie changes and executes callbacks when specified cookies update. Use this to synchronize state across tabs/windows or react to authentication changes.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useCookieListener } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import BrowserOnly from '@docusaurus/BrowserOnly';
14 | import { UseCookieListenerDemo } from '../../demo/UseCookieListenerDemo.tsx';
15 |
16 | Loading...}>
17 | {() => }
18 |
19 |
20 | ```jsx
21 | import { useState } from 'react';
22 | import { useCookieListener } from 'react-haiku';
23 |
24 | export const Component = () => {
25 | const [lastChange, setLastChange] = useState('');
26 |
27 | useCookieListener(
28 | (value, key) => {
29 | setLastChange(`${key} changed to: ${value}`);
30 | },
31 | ['important_cookie'],
32 | );
33 |
34 | return (
35 | <>
36 | Last change: {lastChange || 'None'}
37 | (document.cookie = 'important_cookie=updated')}>
38 | Simulate Change
39 |
40 | >
41 | );
42 | };
43 | ```
44 |
45 | ### API
46 |
47 | #### Arguments
48 |
49 | - `effect` - Callback function receiving (newValue, cookieKey)
50 | - `cookies` - Array of cookie names to monitor
51 |
--------------------------------------------------------------------------------
/docs/static/img/github.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/lib/hooks/useCookieListener.ts:
--------------------------------------------------------------------------------
1 | import { useRef, useEffect } from 'react';
2 |
3 | import { parseToCookieType, getCookies, parseToDataType } from '../helpers/cookie';
4 |
5 | export const useCookieListener = (
6 | effect: (a: T, b: string) => void,
7 | cookies: string[],
8 | ) => {
9 | const cookieValues = useRef>(getCookies(cookies));
10 |
11 | useEffect(() => {
12 | const cookieOnChange = () => {
13 | const currentCookiesValues = getCookies(cookies);
14 |
15 | Object.entries(cookieValues.current).forEach(
16 | ([cookieKey, cookieValue]) => {
17 | const currentCookie = currentCookiesValues[cookieKey];
18 |
19 | if (
20 | parseToCookieType(currentCookie) !== parseToCookieType(cookieValue)
21 | ) {
22 | cookieValues.current = {
23 | ...cookieValues.current,
24 | [cookieKey]: currentCookie,
25 | };
26 |
27 | const parsedValue = parseToDataType(
28 | parseToCookieType(currentCookie),
29 | );
30 | if (parsedValue !== undefined) {
31 | effect(parsedValue, cookieKey);
32 | }
33 | }
34 | },
35 | );
36 | };
37 |
38 | const cookieInterval = setInterval(cookieOnChange, 1000);
39 |
40 | return () => {
41 | clearInterval(cookieInterval);
42 | };
43 | }, [effect, cookies]);
44 | };
45 |
--------------------------------------------------------------------------------
/docs/docs/hooks/useInterval.mdx:
--------------------------------------------------------------------------------
1 | # useInterval()
2 |
3 | The useInterval() hook provides managed interval execution with start/stop controls. Use this for recurring tasks like polls, animations, or delayed state updates.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useInterval } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UseIntervalDemo } from '../../demo/UseIntervalDemo.tsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { useState } from 'react';
19 | import { useInterval } from 'react-haiku';
20 |
21 | export const Component = () => {
22 | const [count, setCount] = useState(0);
23 | const { start, stop } = useInterval(() => {
24 | setCount((c) => c + 1);
25 | }, 1000);
26 |
27 | const handleRestart = () => {
28 | stop();
29 | setTimeout(() => {
30 | setCount(0);
31 | start(1000);
32 | }, 50);
33 | };
34 |
35 | return (
36 | <>
37 | Count: {count}
38 | Stop Counter
39 | handleRestart()}>Restart Counter
40 | >
41 | );
42 | };
43 | ```
44 |
45 | ### API
46 |
47 | #### Arguments
48 |
49 | - `callback` - Function to execute at each interval (required)
50 | - `initialDelay` - Starting interval duration in milliseconds (required)
51 |
52 | #### Returns
53 |
54 | - `start` - Function that (re)starts the interval, accepts optional delay parameter
55 | - `stop` - Function that cancels the current interval
56 |
--------------------------------------------------------------------------------
/lib/utils/ErrorBoundary.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | interface ErrorBoundaryProps {
4 | children: React.ReactNode;
5 | fallback: React.ComponentType<{
6 | error: Error | null;
7 | errorInfo: React.ErrorInfo | null;
8 | retry: () => void;
9 | }>;
10 | }
11 |
12 | interface ErrorBoundaryState {
13 | hasError: boolean;
14 | error: Error | null;
15 | errorInfo: React.ErrorInfo | null;
16 | }
17 |
18 | class ErrorBoundary extends React.Component<
19 | ErrorBoundaryProps,
20 | ErrorBoundaryState
21 | > {
22 | constructor(props: ErrorBoundaryProps) {
23 | super(props);
24 | this.state = {
25 | hasError: false,
26 | error: null,
27 | errorInfo: null,
28 | };
29 | }
30 |
31 | static getDerivedStateFromError(error: Error): Partial {
32 | return {
33 | hasError: true,
34 | error,
35 | };
36 | }
37 |
38 | componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
39 | this.setState({
40 | error: error,
41 | errorInfo: errorInfo,
42 | });
43 | }
44 |
45 | render() {
46 | if (this.state.hasError) {
47 | const Fallback = this.props.fallback;
48 | return (
49 | this.setState({ hasError: false })}
53 | />
54 | );
55 | }
56 |
57 | return this.props.children;
58 | }
59 | }
60 |
61 | export default ErrorBoundary;
62 |
--------------------------------------------------------------------------------
/docs/docs/hooks/useToggle.mdx:
--------------------------------------------------------------------------------
1 | # useToggle()
2 |
3 | The `useToggle()` hook can toggle between a set of two possible values and automatically update the state with the new value, if you want to toggle a boolean's state, see `useBoolToggle()` below.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useToggle } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UseToggleDemo } from '../../demo/UseToggleDemo.jsx';
14 |
15 |
16 |
17 | #### General
18 |
19 | ```jsx
20 | import { useToggle } from 'react-haiku';
21 |
22 | export const Component = () => {
23 | const [theme, toggleTheme] = useToggle('dark', ['dark', 'light']);
24 |
25 | return (
26 | <>
27 | {`Theme: ${theme}`}
28 | toggleTheme()}>Toggle!
29 | >
30 | );
31 | }
32 | ```
33 |
34 | #### Boolean
35 |
36 | ```jsx
37 | import { useBoolToggle } from 'react-haiku';
38 |
39 | export const Component = () => {
40 | const [isTrue, toggleTrue] = useBoolToggle(true) // default initial value is false
41 |
42 | return (
43 | <>
44 | {`State: ${isTrue}`}
45 | toggleTrue()}>Toggle!
46 | >
47 | );
48 | }
49 | ```
50 |
51 | ### API
52 |
53 | This hook accepts the following arguments:
54 | - `initialValue` - the initial preferred value for the toggle state
55 | - `options` - must be an array with strictly 2 elements and must also contain the value set for `initialValue`
--------------------------------------------------------------------------------
/docs/demo/UseGeolocationDemo.jsx:
--------------------------------------------------------------------------------
1 | import { useGeolocation } from 'react-haiku';
2 | import React from 'react';
3 |
4 | export const UseGeolocationDemo = () => {
5 | const { latitude, longitude, error, loading } = useGeolocation({
6 | enableHighAccuracy: true,
7 | timeout: 10000,
8 | });
9 |
10 | return (
11 |
12 |
Geolocation Tracker!
13 |
14 | {loading && (
15 |
16 | Loading location...
17 |
18 | )}
19 |
20 | {error && (
21 |
22 |
Error: {error.message}
23 |
Code: {error.code}
24 |
25 | )}
26 |
27 | {!loading && !error && latitude !== null && longitude !== null && (
28 |
29 |
30 | Latitude: {latitude.toFixed(6)}
31 |
32 |
33 | Longitude: {longitude.toFixed(6)}
34 |
35 |
36 | )}
37 |
38 |
39 | Note: You may need to allow location permission in your browser.
40 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/docs/docs/hooks/useCookie.mdx:
--------------------------------------------------------------------------------
1 | # useCookie()
2 |
3 | The useCookie() hook provides reactive cookie management with automatic synchronization across browser tabs. Use this for persistent storage of simple data that needs to survive page reloads.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useCookie } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import BrowserOnly from '@docusaurus/BrowserOnly';
14 | import { UseCookieDemo } from '../../demo/UseCookieDemo.tsx';
15 |
16 | Loading...}>
17 | {() => }
18 |
19 |
20 | ```jsx
21 | import { useCookie } from 'react-haiku';
22 |
23 | export const Component = () => {
24 | const [userPref, setUserPref, deleteUserPref] = useCookie(
25 | 'theme_preference',
26 | 'light',
27 | 30,
28 | );
29 |
30 | return (
31 | <>
32 | Current theme: {userPref}
33 | setUserPref('light')}>Set Light
34 | setUserPref('dark')}>Set Dark
35 | Reset
36 | >
37 | );
38 | };
39 | ```
40 |
41 | ### API
42 |
43 | #### Arguments
44 |
45 | - `key` - Cookie identifier (required)
46 | - `initialValue` - Default value if cookie doesn't exist (required)
47 | - `expireDays` - Cookie expiration in days (optional, default 365)
48 |
49 | #### Returns
50 |
51 | - `cookieValue` - Current cookie value
52 | - `setValue` - Setter function (accepts new value)
53 | - `deleteValue` - Delete function (removes cookie)
54 |
--------------------------------------------------------------------------------
/docs/src/components/Animations/SplitText.js:
--------------------------------------------------------------------------------
1 | import { useSprings, animated } from '@react-spring/web';
2 | import React from 'react';
3 |
4 | export const SplitText = ({ text, className = '', delay = 100 }) => {
5 | const letters = text.split('');
6 | const [inView, setInView] = React.useState(false);
7 | const ref = React.useRef();
8 |
9 | React.useEffect(() => {
10 | const observer = new IntersectionObserver(
11 | ([entry]) => {
12 | if (entry.isIntersecting) {
13 | setInView(true);
14 | observer.unobserve(ref.current); // Unobserve after triggering the animation
15 | }
16 | },
17 | { threshold: 0.1, rootMargin: '-100px' }
18 | );
19 |
20 | observer.observe(ref.current);
21 |
22 | return () => observer.disconnect();
23 | }, []);
24 |
25 | const springs = useSprings(
26 | letters.length,
27 | letters.map((_, i) => ({
28 | from: { opacity: 0, transform: 'translate3d(0,40px,0)' },
29 | to: async (next) => {
30 | await next({ opacity: 1, transform: 'translate3d(0,0px,0)' });
31 | await next({ opacity: 1, transform: 'translate3d(0,0,0)' });
32 | },
33 | delay: i * delay,
34 | }))
35 | );
36 |
37 | return (
38 |
39 | {springs.map((props, index) => (
40 |
41 | {letters[index] === ' ' ? '\u00A0' : letters[index]}
42 |
43 | ))}
44 |
45 | );
46 | };
47 |
--------------------------------------------------------------------------------
/lib/hooks/useKeyPress.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useMemo, useRef } from 'react';
2 |
3 | const normalizeKey = (key: string) => {
4 | return key.toLowerCase();
5 | };
6 |
7 | export function useKeyPress(
8 | keys: string[],
9 | callback: (e: KeyboardEvent) => void,
10 | ) {
11 | const lastKeyPressed = useRef>(new Set([]));
12 | const keysSet = useMemo(() => {
13 | return new Set(keys.map((key) => normalizeKey(key)));
14 | }, [keys]);
15 |
16 | const handleKeyDown = (e: KeyboardEvent) => {
17 | if (e.repeat) return; // To prevent this function from triggering on key hold e.g. Ctrl hold
18 |
19 | lastKeyPressed.current?.add(normalizeKey(e.key));
20 |
21 | // To bypass TypeScript check for the new ECMAScript method `isSubset`
22 | if ((keysSet as any).isSubsetOf(lastKeyPressed.current)) {
23 | e.preventDefault();
24 | callback(e);
25 | }
26 | };
27 |
28 | const handleKeyUp = (e: KeyboardEvent) => {
29 | lastKeyPressed.current?.delete(normalizeKey(e.key));
30 | };
31 |
32 | const handleBlur = () => {
33 | lastKeyPressed.current?.clear();
34 | };
35 |
36 | useEffect(() => {
37 | window.addEventListener('keydown', handleKeyDown);
38 | window.addEventListener('keyup', handleKeyUp);
39 | window.addEventListener('blur', handleBlur);
40 |
41 | return () => {
42 | window.removeEventListener('keydown', handleKeyDown);
43 | window.removeEventListener('keyup', handleKeyUp);
44 | window.removeEventListener('blur', handleBlur);
45 | };
46 | }, [keysSet, callback]);
47 | }
48 |
--------------------------------------------------------------------------------
/lib/hooks/useIntersectionObserver.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useRef } from "react";
2 |
3 | type IntersectionObserverOptions = {
4 | threshold?: number | number[];
5 | rootMargin?: string;
6 | };
7 |
8 | type UseIntersectionObserverProps = {
9 | animateOnce?: boolean;
10 | options?: IntersectionObserverOptions;
11 | };
12 |
13 | type IntersectionObserverResult = {
14 | observeRef: React.MutableRefObject;
15 | isVisible: boolean;
16 | }
17 |
18 | export const useIntersectionObserver = ({
19 | animateOnce = false,
20 | options = {}
21 | }: UseIntersectionObserverProps = {}): IntersectionObserverResult => {
22 | const observeRef = useRef(null);
23 | const [isVisible, setIsVisible] = useState(false);
24 |
25 | if (typeof IntersectionObserver === "undefined") {
26 | console.warn("IntersectionObserver is not supported in this browser.");
27 | return { observeRef, isVisible: true };
28 | }
29 |
30 | useEffect(() => {
31 | const currentRef = observeRef.current;
32 | if (!currentRef) return;
33 |
34 | const observer = new IntersectionObserver((entries) => {
35 | const entry = entries[0];
36 | if (!entry) return;
37 |
38 | setIsVisible(entry.isIntersecting);
39 |
40 | if (entry.isIntersecting && animateOnce) {
41 | observer.disconnect();
42 | }
43 | }, options);
44 |
45 | observer.observe(currentRef);
46 |
47 | return () => {
48 | observer.disconnect();
49 | };
50 | }, [observeRef, animateOnce, options]);
51 |
52 | return { observeRef, isVisible };
53 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-haiku",
3 | "version": "2.4.1",
4 | "description": "React Hook & Utility Library",
5 | "sideEffects": false,
6 | "main": "./dist/index.js",
7 | "module": "./dist/index.js",
8 | "types": "./dist/index.d.ts",
9 | "homepage": "https://reacthaiku.dev/",
10 | "author": "David Haz, Contributors @ github.com/DavidHDev/react-haiku",
11 | "license": "MIT",
12 | "scripts": {
13 | "publish": "npm publish --access-public",
14 | "test": "jest --watch --coverage",
15 | "build": "tsc"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "git+https://github.com/DavidHDev/react-haiku.git"
20 | },
21 | "keywords": [
22 | "reactjs",
23 | "react",
24 | "utility",
25 | "components",
26 | "javascript",
27 | "hooks",
28 | "state",
29 | "frontend",
30 | "react-hooks"
31 | ],
32 | "exports": {
33 | ".": {
34 | "import": "./dist/index.js",
35 | "require": "./dist/index.js",
36 | "types": "./dist/index.d.ts"
37 | }
38 | },
39 | "peerDependencies": {
40 | "react": ">=16.8.0"
41 | },
42 | "dependencies": {
43 | "react": ">=16.8.0"
44 | },
45 | "devDependencies": {
46 | "@testing-library/jest-dom": "^6.4.8",
47 | "@testing-library/react": "^16.3.0",
48 | "@types/jest": "^29.5.12",
49 | "@types/node": "^20.12.2",
50 | "@types/react": "^18.2.14",
51 | "@types/react-dom": "^18.2.6",
52 | "jest": "^29.7.0",
53 | "jest-environment-jsdom": "^29.7.0",
54 | "ts-jest": "^29.2.4",
55 | "typescript": "^5.1.6"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/lib/tests/hooks/useTitle.test.tsx:
--------------------------------------------------------------------------------
1 | import { renderHook } from '@testing-library/react';
2 | import { useTitle } from '../../hooks/useTitle';
3 |
4 | jest.mock('../../hooks/useIsomorphicLayoutEffect', () => ({
5 | useIsomorphicLayoutEffect: jest.fn((callback) => {
6 | callback();
7 | })
8 | }));
9 |
10 | describe('useTitle', () => {
11 | const originalTitle = document.title;
12 |
13 | afterEach(() => {
14 | document.title = originalTitle;
15 | });
16 |
17 | it('should set document title with valid string', () => {
18 | renderHook(() => useTitle('Test Title'));
19 |
20 | expect(document.title).toBe('Test Title');
21 | });
22 |
23 | it('should trim whitespace from title', () => {
24 | renderHook(() => useTitle(' Trimmed Title '));
25 |
26 | expect(document.title).toBe('Trimmed Title');
27 | });
28 |
29 | it('should not set document title with empty string', () => {
30 | renderHook(() => useTitle(''));
31 |
32 | expect(document.title).toBe(originalTitle);
33 | });
34 |
35 | it('should not set document title with whitespace-only string', () => {
36 | renderHook(() => useTitle(' '));
37 |
38 | expect(document.title).toBe(originalTitle);
39 | });
40 |
41 | it('should update title when title prop changes', () => {
42 | const { rerender } = renderHook(({ title }) => useTitle(title), {
43 | initialProps: { title: 'First Title' }
44 | });
45 |
46 | expect(document.title).toBe('First Title');
47 |
48 | rerender({ title: 'Second Title' });
49 |
50 | expect(document.title).toBe('Second Title');
51 | });
52 | });
53 |
--------------------------------------------------------------------------------
/docs/static/img/open-source.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/lib/hooks/useScript.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | export function useScript(src: string) {
4 | const [status, setStatus] = useState(src ? 'loading' : 'idle');
5 |
6 | useEffect(() => {
7 | if (!src) {
8 | setStatus('idle');
9 |
10 | return;
11 | }
12 |
13 | let script: HTMLScriptElement | null = document.querySelector(
14 | `script[src="${src}"]`,
15 | );
16 |
17 | if (!script) {
18 | script = document.createElement('script');
19 | script.src = src;
20 | script.async = true;
21 | script.setAttribute('data-status', 'loading');
22 |
23 | document.body.appendChild(script);
24 |
25 | const setAttributeFromEvent = (event: Event) => {
26 | script?.setAttribute(
27 | 'data-status',
28 | event.type === 'load' ? 'ready' : 'error',
29 | );
30 | };
31 |
32 | script.addEventListener('load', setAttributeFromEvent);
33 | script.addEventListener('error', setAttributeFromEvent);
34 | } else {
35 | setStatus(script.getAttribute('data-status'));
36 | }
37 |
38 | const setStateFromEvent = (event: Event) =>
39 | setStatus(event.type === 'load' ? 'ready' : 'error');
40 |
41 | script.addEventListener('load', setStateFromEvent);
42 | script.addEventListener('error', setStateFromEvent);
43 |
44 | return () => {
45 | if (script) {
46 | script.removeEventListener('load', setStateFromEvent);
47 | script.removeEventListener('error', setStateFromEvent);
48 | }
49 | };
50 | }, [src]);
51 |
52 | return status;
53 | }
54 |
--------------------------------------------------------------------------------
/docs/docs/hooks/useEventListener.mdx:
--------------------------------------------------------------------------------
1 | # useEventListener()
2 |
3 | The useEventListener() hook lets you quickly add an event to a certain `ref` or the app's `window` if no `ref` is specified
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useEventListener } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UseEventListenerDemo } from '../../demo/UseEventListenerDemo.jsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { useState, useRef } from 'react'
19 | import { useEventListener } from "react-haiku"
20 |
21 | export const Component = () => {
22 | const [countWindow, setCountWindow] = useState(0);
23 | const [countRef, setCountRef] = useState(0);
24 |
25 | // Button Ref
26 | const buttonRef = useRef(null)
27 |
28 | // Event Handlers
29 | const countW = () => setCountWindow(countWindow + 1);
30 | const countR = () => setCountRef(countRef + 1);
31 |
32 | // Example 1: Window Event
33 | useEventListener('scroll', countW);
34 |
35 | // Example 2: Element Event
36 | useEventListener('click', countR, buttonRef);
37 |
38 | return (
39 | <>
40 | Window Event Triggered {countWindow} Times!
41 | Ref Event Triggered {countRef} Times!
42 | Click Me
43 | >
44 | );
45 | }
46 | ```
47 |
48 | ### API
49 |
50 | This hook accepts the following arguments:
51 | - `eventName` - the event string value that you want to set to your ref/window
52 | - `handler` - the handler function for your event
53 | - `element` - the ref you can provide for targetting elements for your event
--------------------------------------------------------------------------------
/docs/docs/utilities/classes.mdx:
--------------------------------------------------------------------------------
1 | # Classes
2 |
3 | The `Classes` component is a utility component that conditionally applies different sets of classes based on multiple independent conditions simultaneously.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { Classes } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { ClassesDemo } from '../../demo/ClassesDemo.jsx';
14 |
15 |
16 |
17 | ```jsx
18 | import React, { useState } from 'react';
19 | import { Classes } from 'react-haiku';
20 |
21 | const Component = () => {
22 | const [hasError, setHasError] = useState(false);
23 | const [isSquared, setIsSquared] = useState(false);
24 | const [isDisabled, setIsDisabled] = useState(false);
25 |
26 | return (
27 |
36 | );
37 | };
38 |
39 | export default Component;
40 | ```
41 |
42 | ### API
43 |
44 | The component accepts the following props:
45 |
46 | - `className` - The initial class name for the element. Defaults to an empty string.
47 | - `toggleClasses` - An object with condition-to-classes mappings. Defaults to an empty object.
48 | - `children` - The content to be rendered inside the element.
49 | - `as (ElementType)`: The type of HTML element to render. Defaults to div. You can specify other elements like section, article, etc.
50 | - `[key: string]: any`: Any additional props to be passed to the element.
51 |
--------------------------------------------------------------------------------
/lib/hooks/useHold.ts:
--------------------------------------------------------------------------------
1 | import { off, on } from '../helpers/event';
2 | import { useCallback, useRef, type MouseEvent, type TouchEvent } from 'react';
3 |
4 | type EventType = MouseEvent | TouchEvent;
5 | const isTouchEvent = (e: EventType): e is TouchEvent => 'touches' in e;
6 |
7 | const preventDefault = (e: EventType) => {
8 | if (!isTouchEvent(e)) {
9 | return;
10 | }
11 |
12 | if (e.touches.length < 2 && e.preventDefault) {
13 | e.preventDefault();
14 | }
15 | };
16 |
17 | export const useHold = (
18 | callback: (e: EventType) => any,
19 | { doPreventDefault = true, delay = 1000 } = {},
20 | ) => {
21 | const timeout = useRef();
22 | const target = useRef();
23 |
24 | const start = useCallback(
25 | (event: EventType) => {
26 | if (doPreventDefault && event.target) {
27 | on(event.target, 'touchend', preventDefault, { passive: false });
28 | target.current = event.target;
29 | }
30 |
31 | timeout.current = setTimeout(
32 | () => callback(event),
33 | delay,
34 | ) as unknown as number;
35 | },
36 | [callback, delay, doPreventDefault],
37 | );
38 |
39 | const clear = useCallback(() => {
40 | timeout.current && clearTimeout(timeout.current);
41 |
42 | if (doPreventDefault && target.current) {
43 | off(target.current, 'touchend', preventDefault);
44 | }
45 | }, [doPreventDefault]);
46 |
47 | return {
48 | onMouseDown: (e: MouseEvent) => start(e),
49 | onTouchStart: (e: TouchEvent) => start(e),
50 | onMouseUp: clear,
51 | onMouseLeave: clear,
52 | onTouchEnd: clear,
53 | };
54 | };
55 |
--------------------------------------------------------------------------------
/docs/demo/UseScrollDeviceDemo.tsx:
--------------------------------------------------------------------------------
1 | import { useScrollDevice } from 'react-haiku';
2 | import React from 'react';
3 |
4 | export const UseScrollDeviceDemo = () => {
5 | const device = useScrollDevice();
6 |
7 | return (
8 |
9 |
Scroll Detection:
10 |
11 |
19 | {device?.toUpperCase() || 'SCROLL TO DETECT'}
20 |
21 |
22 |
32 |
33 |
34 |
Mouse Wheel
35 |
36 |
37 |
43 |
44 |
45 |
53 | Scroll vertically to detect input device
54 |
55 |
56 | );
57 | };
58 |
--------------------------------------------------------------------------------
/docs/docs/utilities/show.mdx:
--------------------------------------------------------------------------------
1 | # Show
2 |
3 | The `Show` component can be used for complex conditional rendering. The component can be extended by multiple `When` components and an `Else` component to render their contents based on the conditions you provide.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { Show } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { ShowDemo } from '../../demo/ShowDemo.jsx';
14 |
15 |
16 |
17 | ```jsx
18 | import { useState } from 'react';
19 | import { Show } from 'react-haiku';
20 |
21 | export const Component = () => {
22 | const [number, setNumber] = useState(6);
23 |
24 | return (
25 |
26 |
27 | Number is 6!
28 | setNumber(number + 1)}>Increment
29 |
30 |
31 |
32 | Number is 7!
33 | setNumber(number + 1)}>Increment
34 |
35 |
36 |
37 | No valid number found!
38 | setNumber(6)}>Reset
39 |
40 |
41 | );
42 | }
43 | ```
44 |
45 | ### API
46 |
47 | The `Show` component accepts two types of child components:
48 |
49 | - `When` - this component can be used multiple times. It renders its contents when the condition passed inside its `isTrue` prop is evaluated as truthy
50 | - `Else` - this component can only be used once and will render its contents when none of the `When` components have met their conditions.
--------------------------------------------------------------------------------
/docs/demo/UseWebSocketDemo.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { useWebSocket } from 'react-haiku';
3 |
4 | export const UseWebSocketDemo = () => {
5 | const [newValue, setNewValue] = useState('');
6 |
7 | const { status, lastMessage, sendMessage } = useWebSocket(
8 | 'wss://echo.websocket.org',
9 | );
10 |
11 | useEffect(() => {
12 | if (status === 'OPEN') {
13 | sendMessage('useWebSocket() with Haiku!');
14 | }
15 | }, [status]);
16 |
17 | return (
18 |
19 |
20 | Web Socket Status: {status}
21 |
22 |
23 | setNewValue(e.target.value)}
30 | />
31 | sendMessage(newValue)}
34 | >
35 | Send
36 |
37 |
38 |
44 |
Last Message:
45 |
46 | {' '}
47 |
48 | {lastMessage ? lastMessage : 'No message yet'}
49 |
50 |
51 |
52 |
53 | );
54 | };
55 |
--------------------------------------------------------------------------------
/docs/docs/hooks/useIdle.mdx:
--------------------------------------------------------------------------------
1 | # useIdle()
2 |
3 | The `useIdle()` hook lets you detect current user activity or inactivity on a web page, returning a boolean value that represents whether or not the user is currently active. The user is set as inactive when no events are triggered after a specified delay.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useIdle } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | #### Default Options
14 |
15 | import { UseIdleDemo } from '../../demo/UseIdleDemo.jsx';
16 |
17 |
18 |
19 |
20 | ```jsx
21 | import { useIdle } from "react-haiku"
22 |
23 | export const Component = () => {
24 | const idle = useIdle(3000);
25 |
26 | return Current Status: {idle ? 'Idle' : 'Active'}
27 | }
28 | ```
29 |
30 | #### Custom Options
31 |
32 | import { UseIdleCustomDemo } from '../../demo/UseIdleDemo.jsx';
33 |
34 |
35 |
36 |
37 | ```jsx
38 | import { useIdle } from "react-haiku"
39 |
40 | export const Component = () => {
41 | const idle = useIdle(1000, { events: ['click', 'touchstart'], initialState: false });
42 |
43 | return (
44 | <>
45 | Works only with click/touch events!
46 | Current Status: {idle ? 'Idle' : 'Active'}
47 | >
48 | );
49 | }
50 | ```
51 |
52 |
53 | ### API
54 |
55 | This hook accepts the following arguments:
56 |
57 | - `timeout` - the time until the state is set as inactive
58 | - `options` - the options object you can pass in
59 | - `events` - string array, the collection of events that will trigger activity signals, by default: `['keypress', 'mousemove', 'touchmove', 'click', 'scroll']`
60 | - `initialState` - boolean, the initial activity state
--------------------------------------------------------------------------------
/docs/docs/hooks/useFullscreen.mdx:
--------------------------------------------------------------------------------
1 | # useFullscreen()
2 |
3 | The `useFullscreen()` hook can toggle between entering fullscreen mode and exiting fullscreen mode.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useFullscreen } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UseFullscreenDemo } from '../../demo/UseFullscreenDemo.jsx';
14 |
15 |
16 |
17 | #### For Overall Document
18 |
19 | ```jsx
20 |
21 | import { useEffect, useRef } from 'react'
22 | import { useFullscreen } from 'react-haiku';
23 |
24 | export const Component = () => {
25 | const documentRef = useRef(null);
26 |
27 | useEffect(() => {
28 | documentRef.current = document.documentElement;
29 | }, []);
30 |
31 | const {isFullscreen, toggleFullscreen } = useFullscreen(documentRef);
32 | return (
33 |
34 | Is in Fullscreen Mode: {isFullscreen ? "True" : "False"}
35 | Toggle Fullscreen!
36 |
37 | );
38 | }
39 |
40 | ```
41 |
42 | #### For Specific Element
43 |
44 | ```jsx
45 |
46 | import { useEffect } from 'react'
47 | import { useFullscreen } from 'react-haiku';
48 |
49 | export const Component = () => {
50 | const elementRef = useRef(null);
51 | const {isFullscreen, toggleFullscreen } = useFullscreen(elementRef);
52 | return (
53 |
54 | Is in Fullscreen Mode: {isFullscreen ? "True" : "False"}
55 | Toggle Fullscreen!
56 |
57 | );
58 | }
59 |
60 | ```
61 |
62 | ### API
63 |
64 | This hook accepts the following arguments:
65 | - `targetRef` - a reference to the DOM element you want to toggle fullscreen for.
--------------------------------------------------------------------------------
/docs/docs/hooks/useTImer.mdx:
--------------------------------------------------------------------------------
1 | # useTimer()
2 |
3 | The `useTimer` hook provides a simple way to manage a timer with start, pause, and reset functionalities. It supports both counting up and counting down with a customizable interval.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useTimer } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import BrowserOnly from '@docusaurus/BrowserOnly';
14 | import { UseTimerDemo } from '../../demo/UseTimerDemo.jsx';
15 |
16 | Loading...}>
17 | {() => }
18 |
19 |
20 | ```jsx
21 | import { useTimer } from 'react-haiku';
22 | export const TimerComponent = () => {
23 | const { time, isRunning, start, pause, reset } = useTimer({
24 | startTime: 0,
25 | endTime: 10,
26 | interval: 1000,
27 | });
28 |
29 | return (
30 |
31 |
Time: {time}
32 |
33 | Start
34 |
35 |
36 | Pause
37 |
38 |
Reset
39 |
40 | );
41 | };
42 | ```
43 |
44 | ### API
45 |
46 | This hook accepts the following arguments:
47 |
48 | - `startTime` - the initial time value for the timer
49 | - `endTime` - the end time value for the timer
50 | - `interval` (number, default: `1000`)- the interval in milliseconds to update the timer value
51 |
52 | This hook returns an object with the following properties:
53 |
54 | - `time` - the current time value of the timer
55 | - `isRunning` - a boolean value indicating whether the timer is running
56 | - `start` - a function to start the timer
57 | - `pause` - a function to pause the timer
58 | - `reset` - a function to reset the timer
59 |
--------------------------------------------------------------------------------
/docs/docs/hooks/useIntersectionObserver.mdx:
--------------------------------------------------------------------------------
1 | # useIntersectionObserver()
2 |
3 | The `useIntersectionObserver` hook provides a way to detect when an element enters or exits the viewport. It offers options for configuring intersection thresholds, margins, and one-time animation triggers.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useIntersectionObserver } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import BrowserOnly from '@docusaurus/BrowserOnly';
14 | import { UseIntersectionObserverDemo } from '../../demo/UseIntersectionObserverDemo.jsx';
15 |
16 | Loading...}>
17 | {() => }
18 |
19 |
20 | ```jsx
21 |
22 | import { useIntersectionObserver } from 'react-haiku';
23 |
24 | export const Component = () => {
25 | const {observeRef, isVisible} = useIntersectionObserver({
26 | animateOnce: false,
27 | options:{
28 | threshold: .5,
29 | rootMargin: '-40% 0px -40% 0px'
30 | }
31 | })
32 | return (
33 |
34 |
35 | I'm being observed!
36 |
37 |
38 | );
39 | }
40 |
41 | ```
42 |
43 | ### API
44 |
45 | This hook accepts the following arguments:
46 |
47 | - `animateOnce` (optional) - boolean indicating whether the element should be observed only once (`true`) or continuously (`false`), the default value for this argument is `false`.
48 | - `options` - an object containing IntersectionObserver options:
49 | - `threshold` (optional) - a single number or an array of numbers representing the percentage of the target's visibility the observer's callback should trigger.
50 | - `rootMargin` (optional) - a string which defines a margin around the root. This can be used to grow or shrink the area used for intersection detection.
--------------------------------------------------------------------------------
/docs/docs/hooks/usePermission.mdx:
--------------------------------------------------------------------------------
1 | # usePermission()
2 |
3 | The `usePermission()` check browser permissions for querying state for various browser APIs
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { usePermission } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { UsePermissionDemo } from '../../demo/UsePermissionDemo.tsx';
14 |
15 |
16 |
17 | ```tsx
18 | import { useState } from 'react';
19 | import { usePermission } from 'react-haiku';
20 |
21 | export const Component = () => {
22 | const state = usePermission("geolocation")
23 | const [location, setLocation] = useState(null)
24 |
25 | function handleGetCurrentPosition() {
26 | if (state !== "prompt" && state !== "granted") return
27 |
28 | navigator.geolocation.getCurrentPosition((location) => {
29 | setLocation(location)
30 | })
31 | }
32 |
33 | return (
34 |
35 |
Permission state: {state}
36 |
37 |
38 | {JSON.stringify(location ?? {}, null, 2)}
39 |
40 |
41 |
42 |
43 | Get current position
44 |
45 |
46 |
47 | );
48 | }
49 | ```
50 |
51 | ### API
52 |
53 | #### Arguments
54 |
55 | - `permission` - The name of the API whose permissions you want to query, such as the `PermissionName` and other described in the [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Permissions/query#name) documentation.
56 |
57 | #### Returns
58 |
59 | The the state of a requested permission, combining the standard `PermissionState` with additional internal states.
60 | - `checking`: The permission state is being verified.
61 | - `not-supported`: The permission check is not supported or an error occurred during verification.
62 |
--------------------------------------------------------------------------------
/docs/docs/utilities/class.mdx:
--------------------------------------------------------------------------------
1 | # Class
2 |
3 | The `Class` component is a utility component that conditionally applies a CSS class to a `div` element based on a boolean condition. It is useful for toggling styles dynamically.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { Class } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { ClassDemo } from '../../demo/ClassDemo.jsx';
14 |
15 |
16 |
17 | ```jsx
18 | import React, { useState } from 'react';
19 | import { Class } from 'react-haiku';
20 |
21 | const Component = () => {
22 | const [isActive, setIsActive] = useState(false);
23 |
24 | const toggleActive = () => {
25 | setIsActive(!isActive);
26 | };
27 |
28 | return (
29 |
30 |
31 | {isActive ? 'Deactivate' : 'Activate'}
32 |
33 |
34 |
40 | This is a box that will toggle its class based on the button click.
41 |
42 |
43 | );
44 | };
45 |
46 | export default Component;
47 | ```
48 |
49 | ### API
50 |
51 | The component accepts the following props:
52 |
53 | - `className` - The initial class name for the `div` element. Defaults to an empty string.
54 | - `condition` - The condition to determine whether to apply the `toggleClass` or not. Defaults to `false`.
55 | - `toggleClass` - The class name to be toggled based on the condition. Defaults to an empty string.
56 | - `children` - The content to be rendered inside the `div` element.
57 | - `as (ElementType)`: The type of HTML element to render. Defaults to div. You can specify other elements like section, article, etc.
58 | - `[key: string]: any`: Any additional props to be passed to the element.
59 |
--------------------------------------------------------------------------------
/lib/hooks/useLocalStorage.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useRef, useState } from 'react';
2 | import { useEventListener } from './useEventListener';
3 |
4 | export function useLocalStorage(key: string, initialValue: T) {
5 | const readValue = useCallback(() => {
6 | if (typeof window === 'undefined') {
7 | return initialValue;
8 | }
9 |
10 | try {
11 | const item = window.localStorage.getItem(key);
12 |
13 | return item ? parseJSON(item) : initialValue;
14 | } catch (error) {
15 | console.error(`Error getting storage key “${key}”:`, error);
16 |
17 | return initialValue;
18 | }
19 | }, [initialValue, key]);
20 |
21 | const [storedValue, setStoredValue] = useState(readValue);
22 | const setValueRef = useRef();
23 |
24 | setValueRef.current = (value: T) => {
25 | try {
26 | const newValue = value instanceof Function ? value(storedValue) : value;
27 | window.localStorage.setItem(key, JSON.stringify(newValue));
28 |
29 | setStoredValue(newValue);
30 | window.dispatchEvent(new Event('local-storage'));
31 | } catch (error) {
32 | console.warn(`Error adding "${key}" to storage:`, error);
33 | }
34 | };
35 |
36 | const setValue = useCallback((value: T) => setValueRef.current?.(value), []);
37 |
38 | useEffect(() => {
39 | setStoredValue(readValue());
40 | }, []);
41 |
42 | const handleStorageChange = useCallback(
43 | () => setStoredValue(readValue()),
44 | [readValue],
45 | );
46 | useEventListener('storage', handleStorageChange);
47 | useEventListener('local-storage', handleStorageChange);
48 | return [storedValue, setValue];
49 | }
50 |
51 | function parseJSON(value: any) {
52 | try {
53 | return value === 'undefined' ? undefined : JSON.parse(value ?? '');
54 | } catch {
55 | return undefined;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/lib/hooks/useScrollDevice.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 |
3 | type ScrollDevice = "mouse" | "trackpad" | null;
4 |
5 | export function useScrollDevice(): ScrollDevice {
6 | const [deviceType, setDeviceType] = useState(null);
7 | let debounceTimeout: ReturnType | null = null;
8 |
9 | useEffect(() => {
10 | let lastEventTimestamp = 0;
11 | let lastDeltaY = 0;
12 |
13 | const detectScrollDevice = (event: WheelEvent) => {
14 | const now = performance.now();
15 | const timeDiff = now - lastEventTimestamp;
16 |
17 | const isTrackpad = (
18 | Math.abs(event.deltaY) < 50 || // Small delta values indicate trackpad
19 | (Math.abs(event.deltaX) > 0 && Math.abs(event.deltaY) < 50) // Trackpads scroll in both directions
20 | );
21 |
22 | const isMouseWheel = (
23 | Math.abs(event.deltaY) >= 50 || // Large jumps indicate mouse wheel
24 | (timeDiff > 50 && Math.abs(event.deltaY - lastDeltaY) < 10) // Consistent jumps suggest a wheel
25 | );
26 |
27 | const detectedDevice = isTrackpad ? "trackpad" : isMouseWheel ? "mouse" : null;
28 |
29 | lastEventTimestamp = now;
30 | lastDeltaY = event.deltaY;
31 |
32 | // Debounce: Ensure only the last detected value is stored
33 | if (debounceTimeout) {
34 | clearTimeout(debounceTimeout);
35 | }
36 |
37 | debounceTimeout = setTimeout(() => {
38 | setDeviceType(detectedDevice);
39 | }, 200); // Adjust debounce time if needed
40 | };
41 |
42 | window.addEventListener("wheel", detectScrollDevice);
43 |
44 | return () => {
45 | window.removeEventListener("wheel", detectScrollDevice);
46 | if (debounceTimeout) {
47 | clearTimeout(debounceTimeout);
48 | }
49 | };
50 | }, []);
51 |
52 | return deviceType;
53 | }
54 |
--------------------------------------------------------------------------------
/lib/hooks/useScreenSize.ts:
--------------------------------------------------------------------------------
1 | import { useState, useCallback, useLayoutEffect } from 'react';
2 |
3 | const breakpoints = {
4 | xs: 639,
5 | sm: 767,
6 | md: 1023,
7 | lg: 1279,
8 | xl: 1535,
9 | '2xl': Infinity,
10 | };
11 |
12 | const getBreakpoint = (width: number): keyof typeof breakpoints => {
13 | return (Object.keys(breakpoints) as (keyof typeof breakpoints)[])
14 | .find(bp => width <= breakpoints[bp]) ?? '2xl';
15 | };
16 |
17 | export const useScreenSize = () => {
18 | const [screenSize, setScreenSize] = useState(() =>
19 | typeof window === 'undefined' ? 'xl' : getBreakpoint(window.innerWidth),
20 | );
21 |
22 | const handleResize = useCallback(() => {
23 | setScreenSize(getBreakpoint(window.innerWidth));
24 | }, []);
25 |
26 | useLayoutEffect(() => {
27 | if (typeof window === 'undefined') return;
28 |
29 | let ticking = false;
30 |
31 | const onResize = () => {
32 | if (ticking) return;
33 | ticking = true;
34 | requestAnimationFrame(() => {
35 | handleResize();
36 | ticking = false;
37 | });
38 | };
39 |
40 | // Initial measurement
41 | handleResize();
42 | window.addEventListener('resize', onResize);
43 |
44 | return () => window.removeEventListener('resize', onResize);
45 | }, [handleResize]);
46 |
47 | const eq = (bp: keyof typeof breakpoints) => screenSize === bp;
48 | const lt = (bp: keyof typeof breakpoints) => breakpoints[screenSize] < breakpoints[bp];
49 | const gt = (bp: keyof typeof breakpoints) => breakpoints[screenSize] > breakpoints[bp];
50 | const lte = (bp: keyof typeof breakpoints) => eq(bp) || lt(bp);
51 | const gte = (bp: keyof typeof breakpoints) => eq(bp) || gt(bp);
52 |
53 |
54 | return {
55 | eq,
56 | lt,
57 | gt,
58 | lte,
59 | gte,
60 | toString: () => screenSize,
61 | };
62 | };
63 |
--------------------------------------------------------------------------------
/docs/demo/UseCookieDemo.tsx:
--------------------------------------------------------------------------------
1 | import { useCookie } from 'react-haiku';
2 | import React, { useState } from 'react';
3 |
4 | export const UseCookieDemo = () => {
5 | const [newValue, setNewValue] = useState('');
6 |
7 | const [cookieValue, setCookie, deleteCookie] = useCookie(
8 | 'demo_cookie',
9 | 'default',
10 | 7,
11 | ) as [string, (value: string) => void, () => void];
12 |
13 | return (
14 |
15 |
Cookie Value:
16 |
{cookieValue}
17 |
18 |
19 | setNewValue(e.target.value)}
26 | />
27 |
28 | setCookie(newValue)}
31 | >
32 | Set Custom Value
33 |
34 |
35 |
36 |
37 | setCookie(Date.now().toString())}
40 | >
41 | Set Timestamp
42 |
43 |
44 |
45 | Delete Cookie
46 |
47 |
48 |
49 |
57 | Open DevTools → Application → Cookies to see persistence
58 |
59 |
60 | );
61 | };
62 |
--------------------------------------------------------------------------------
/docs/docs/hooks/useGeolocation.mdx:
--------------------------------------------------------------------------------
1 | # useGeolocation()
2 |
3 | The `useGeolocation()` hook provides access to the user's current geographical location using the browser's Geolocation API.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useGeolocation } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import BrowserOnly from '@docusaurus/BrowserOnly';
14 | import { UseGeolocationDemo } from '../../demo/UseGeolocationDemo.jsx';
15 |
16 | Loading...}>
17 | {() => }
18 |
19 |
20 | ```jsx
21 | import { useGeolocation } from 'react-haiku';
22 |
23 | export const Component = () => {
24 | const { latitude, longitude, error, loading } = useGeolocation({
25 | enableHighAccuracy: true,
26 | timeout: 10000,
27 | });
28 |
29 | if (loading) {
30 | return Getting your location...
;
31 | }
32 |
33 | if (error) {
34 | return Error: {error.message}
;
35 | }
36 |
37 | return (
38 |
39 |
Latitude: {latitude}
40 |
Longitude: {longitude}
41 |
42 | );
43 | };
44 | ```
45 |
46 | ### API
47 |
48 | This hook accepts an optional configuration object with the following properties:
49 |
50 | - `enableHighAccuracy` - boolean to request the most accurate results possible, defaults to `false`
51 | - `timeout` - maximum time in milliseconds to wait before timeout, defaults to `5000`
52 | - `maximumAge` - maximum age of cached position in milliseconds, defaults to `0`
53 | - `watch` - boolean to continuously watch for position changes, defaults to `false`
54 |
55 | The hook returns an object with:
56 |
57 | - `latitude` - the latitude coordinate or `null`
58 | - `longitude` - the longitude coordinate or `null`
59 | - `error` - error object with `code` and `message` properties, or `null`
60 | - `loading` - boolean indicating if the request is in progress
61 |
--------------------------------------------------------------------------------
/lib/hooks/useBatteryStatus.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | // Define BatteryManager type manually
4 | interface BatteryManager extends EventTarget {
5 | charging: boolean;
6 | level: number;
7 | addEventListener(
8 | type: 'chargingchange' | 'levelchange',
9 | listener: (this: BatteryManager, ev: Event) => any
10 | ): void;
11 | removeEventListener(
12 | type: 'chargingchange' | 'levelchange',
13 | listener: (this: BatteryManager, ev: Event) => any
14 | ): void;
15 | }
16 |
17 | type NavigatorWithBattery = Navigator & {
18 | getBattery?: () => Promise;
19 | };
20 |
21 | export function useBatteryStatus() {
22 | const [batteryStatus, setBatteryStatus] = useState({
23 | level: 0,
24 | isCharging: false,
25 | });
26 |
27 | useEffect(() => {
28 | const navigatorWithBattery = navigator as NavigatorWithBattery;
29 |
30 | if (!navigatorWithBattery.getBattery) {
31 | console.warn("Battery Status API is not supported in this browser.");
32 | return;
33 | }
34 |
35 | let battery: BatteryManager | null = null;
36 |
37 | const updateBatteryStatus = () => {
38 | if (battery) {
39 | setBatteryStatus({
40 | level: Math.round(battery.level * 100),
41 | isCharging: battery.charging,
42 | });
43 | }
44 | };
45 |
46 | navigatorWithBattery.getBattery().then((bat) => {
47 | battery = bat;
48 | updateBatteryStatus();
49 |
50 | battery.addEventListener('chargingchange', updateBatteryStatus);
51 | battery.addEventListener('levelchange', updateBatteryStatus);
52 | });
53 |
54 | return () => {
55 | if (battery) {
56 | battery.removeEventListener('chargingchange', updateBatteryStatus);
57 | battery.removeEventListener('levelchange', updateBatteryStatus);
58 | }
59 | };
60 | }, []);
61 |
62 | return batteryStatus;
63 | }
64 |
--------------------------------------------------------------------------------
/lib/tests/utils/Classes.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import { Classes } from '../../utils/Classes';
3 |
4 | describe('Classes component', () => {
5 | const text = 'Classes Block';
6 |
7 | it('renders the Classes component', () => {
8 | const { asFragment } = render(
9 | {text} ,
10 | );
11 | expect(asFragment()).toMatchSnapshot();
12 | expect(screen.getByText(text)).toBeInTheDocument();
13 | });
14 |
15 | it('renders with base className', () => {
16 | render({text} );
17 |
18 | const element = screen.queryByText(text);
19 | expect(element).toHaveClass('base-class');
20 | });
21 |
22 | it('applies toggleClasses when condition is true', () => {
23 | render(
24 |
29 | {text}
30 | ,
31 | );
32 | const element = screen.getByText(text);
33 | expect(element).toHaveClass('base-class active-class');
34 | });
35 |
36 | it('does not apply toggleClasses when condition is false', () => {
37 | render(
38 |
42 | {text}
43 | ,
44 | );
45 | const element = screen.getByText(text);
46 | expect(element).not.toHaveClass('inactive-class');
47 | });
48 |
49 | it('removes duplicate classes', () => {
50 | render(
51 |
55 | {text}
56 | ,
57 | );
58 | const element = screen.getByText(text);
59 | expect(element).toHaveClass('base-class active-class focus');
60 | });
61 | });
62 |
--------------------------------------------------------------------------------
/docs/demo/ClassesDemo.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Classes } from 'react-haiku';
3 |
4 | export const ClassesDemo = () => {
5 | const [hasError, setHasError] = useState(false);
6 | const [isSquared, setIsSquared] = useState(false);
7 | const [isDisabled, setIsDisabled] = useState(false);
8 |
9 | return (
10 |
61 | );
62 | };
63 |
--------------------------------------------------------------------------------
/docs/static/img/install-bg.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/lib/helpers/cookie.ts:
--------------------------------------------------------------------------------
1 | export const parseToDataType = (
2 | value: string | undefined,
3 | isItRetry = false,
4 | ): T | undefined => {
5 | try {
6 | return value === "undefined" || value === undefined
7 | ? undefined
8 | : (JSON.parse(value) as T);
9 | } catch (e) {
10 | if (!isItRetry) {
11 | return parseToDataType(`"${value?.replaceAll?.('"', "")}"`, true);
12 | }
13 |
14 | return undefined;
15 | }
16 | };
17 |
18 | export const parseToCookieType = (value: T) => {
19 | if (typeof value === "string") {
20 | return value;
21 | }
22 |
23 | return JSON.stringify(value);
24 | };
25 |
26 | export const getCookie = (name: string): T | undefined => {
27 | const value = `; ${document.cookie}`;
28 |
29 | const [_, cookie] = value.split(`; ${name}=`);
30 |
31 | return cookie ? parseToDataType(cookie.split(";")[0]) : undefined;
32 | };
33 |
34 | export const getCookies = = Record>(
35 | cookies: string[] = [],
36 | ): T => {
37 | if (cookies.length) {
38 | return cookies.reduce>(
39 | (result, cookie) => ({
40 | ...result,
41 | [cookie]: getCookie(cookie),
42 | }),
43 | {},
44 | ) as T;
45 | }
46 |
47 | return Object.fromEntries(
48 | document.cookie.split("; ").map((c) => {
49 | const [key, value] = c.split("=");
50 | return [key, parseToDataType(value)];
51 | }),
52 | ) as T;
53 | };
54 |
55 | export const setCookie = (name: string, value: T, expireDays: number) => {
56 | const date = new Date();
57 | const millisecondsInADay = 24 * 60 * 60 * 1000;
58 | const expireDate = date.getTime() + expireDays * millisecondsInADay;
59 |
60 | date.setTime(expireDate);
61 |
62 | const expires = `expires=${date.toUTCString()}`;
63 |
64 | document.cookie = `${name}=${parseToCookieType(value)}; ${expires}; path=/;`;
65 | };
66 |
67 | export const deleteCookie = (name: string) => {
68 | document.cookie = `${name}=; path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
69 | };
70 |
71 |
72 |
--------------------------------------------------------------------------------
/docs/docs/hooks/useTabNotification.mdx:
--------------------------------------------------------------------------------
1 | # useTabNotification()
2 |
3 | The useTabNotification() hook manages browser tab notifications through title modifications and favicon indicators. Use this to alert users of background activity or new notifications when they're in another tab.
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { useTabNotification } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import BrowserOnly from '@docusaurus/BrowserOnly';
14 | import { UseTabNotificationDemo } from '../../demo/UseTabNotificationDemo.tsx';
15 |
16 | Loading...}>
17 | {() => }
18 |
19 |
20 | ```jsx
21 | import { useTabNotification } from 'react-haiku';
22 |
23 | export const Component = () => {
24 | const {
25 | setTitlePrefix,
26 | setFlashMessage,
27 | setIsShown,
28 | setCustomTitle,
29 | setShowFaviconDot,
30 | setFaviconDotColor,
31 | } = useTabNotification();
32 |
33 | return (
34 | <>
35 | setIsShown(true)}>Show Notification
36 |
37 | setCustomTitle(e.target.value)}
41 | />
42 |
43 | {
45 | setTitlePrefix('[ALERT]');
46 | setFlashMessage('New message!');
47 | setShowFaviconDot(true);
48 | }}
49 | >
50 | Trigger Alert
51 |
52 | >
53 | );
54 | };
55 | ```
56 |
57 | ### API
58 |
59 | #### Arguments
60 |
61 | - `flashDelayInSeconds` - Alternate interval for title flashing (optional, default 2)
62 |
63 | #### Returns
64 |
65 | - `setTitlePrefix` - Adds persistent prefix to page title
66 | - `setFlashMessage` - Sets message to alternate with original title
67 | - `setIsShown` - Toggles notification visibility
68 | - `setCustomTitle` - Overrides default page title
69 | - `setShowFaviconDot` - Toggles favicon indicator
70 | - `setFaviconDotColor` - Sets favicon dot color (hex format)
71 |
--------------------------------------------------------------------------------
/lib/utils/Classes.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, ElementType } from 'react';
2 |
3 | // Check if a value is a plain object literal
4 | const isObjectLiteral = (value: any): boolean => {
5 | return Object.prototype.toString.call(value) === '[object Object]';
6 | };
7 |
8 | // Remove duplicate class names from a string
9 | const deduplicateClasses = (classes: string) =>
10 | [...new Set(classes.split(' '))].join(' ');
11 |
12 | interface ClassProps {
13 | className?: string;
14 | toggleClasses?: Record;
15 | children: ReactNode;
16 | as?: ElementType;
17 | [key: string]: any;
18 | }
19 |
20 | /**
21 | * Classes component that conditionally applies class names to a specified HTML element.
22 | *
23 | * @param className - The initial class name for the element.
24 | * @param toggleClasses - An object with condition-to-classes mappings.
25 | * @param children - The content to be rendered inside the specified HTML element.
26 | * @param as - The type of HTML element to render.
27 | * @param props - Any additional props to be passed to the element.
28 | * @returns The rendered Class component.
29 | */
30 |
31 | export const Classes: React.FC = ({
32 | className = '',
33 | toggleClasses = {},
34 | children,
35 | as: Component = 'div',
36 | ...props
37 | }) => {
38 | if (!isObjectLiteral(toggleClasses)) {
39 | console.error(
40 | 'toggleClasses prop must be an object literal. Received:',
41 | toggleClasses,
42 | );
43 |
44 | return (
45 |
46 | {children}
47 |
48 | );
49 | }
50 |
51 | let computedClassNames = className;
52 |
53 | for (const [classes, condition] of Object.entries(toggleClasses)) {
54 | if (condition) {
55 | computedClassNames += ` ${classes.trim()}`;
56 | }
57 | }
58 |
59 | computedClassNames = deduplicateClasses(computedClassNames);
60 |
61 | return (
62 |
63 | {children}
64 |
65 | );
66 | };
67 |
--------------------------------------------------------------------------------
/docs/docs/utilities/errorBoundary.mdx:
--------------------------------------------------------------------------------
1 | # Error Boundary
2 |
3 | The `ErrorBoundary` component is a utility component that catches errors in its child component tree, logs the error, and prevents the entire application from crashing by rendering a fallback UI instead..
4 |
5 | ### Import
6 |
7 | ```jsx
8 | import { ErrorBoundary } from 'react-haiku';
9 | ```
10 |
11 | ### Usage
12 |
13 | import { ErrorBoundaryDemo } from '../../demo/ErrorBoundaryDemo.tsx';
14 |
15 |
16 |
17 | ```tsx
18 | import React from 'react';
19 | import { ErrorBoundary } from 'react-haiku';
20 |
21 | // Component that will intentionally cause an error
22 | const CrashComponent: React.FC = () => {
23 | throw new Error('This is a test error inside the ErrorBoundary!');
24 | };
25 |
26 | export const ErrorBoundaryDemo: React.FC = () => {
27 | return (
28 |
29 |
30 |
31 | );
32 | };
33 | ```
34 |
35 | ### Fallback
36 |
37 | You can access `retry`, `error` and `errorInfo` prop in you Fallback component which you can use to create a dynamic UI.
38 |
39 | ```tsx
40 | interface FallbackProps {
41 | retry: () => void;
42 | error: Error | null;
43 | errorInfo: React.ErrorInfo | null;
44 | }
45 |
46 | const Fallback: React.FC = ({ retry }) => {
47 | return (
48 |
49 | We faced an error
50 |
55 | Retry
56 |
57 |
58 | );
59 | };
60 | ```
61 |
62 | ### API
63 |
64 | The **ErrorBoundary** accepts the following props:
65 |
66 | - `fallback` - Component to render when an error is occured in child components.
67 |
68 | The **Fallback** receives the following props:
69 |
70 | - `retry` - Function to retry/reload the wrapped component.
71 | - `error` - error of type **Error**
72 | - `errorInfo` - error info of type **React.ErrorInfo**
73 |
--------------------------------------------------------------------------------
/docs/demo/SwitchDemo.tsx:
--------------------------------------------------------------------------------
1 | import { Switch } from 'react-haiku';
2 | import React from 'react';
3 |
4 | enum Reaction {
5 | LIKE = 'like',
6 | FIRE = 'fire',
7 | LOVE = 'love',
8 | }
9 |
10 | const CaseReactionLike = () => 👍
11 | const CaseReactionFire = () => 🔥
12 | const CaseReactionLove = () => ❤️
13 | const CaseDefault = () => 🚀
14 |
15 | export const SwitchDemo = () => {
16 | const [reaction, setReaction] = React.useState();
17 |
18 | const handleReact = (e: React.ChangeEvent) => {
19 | const reactionSelected = e.target.value as Reaction;
20 | setReaction({
21 | [Reaction.LIKE]: Reaction.LIKE,
22 | [Reaction.FIRE]: Reaction.FIRE,
23 | [Reaction.LOVE]: Reaction.LOVE
24 | }[reactionSelected]);
25 | }
26 |
27 | return (
28 |
29 |
30 |
React to this component:
31 |
32 |
33 | Default
34 | 👍 Like
35 | 🔥 Fire
36 | ❤️ Love
37 |
38 |
39 |
40 |
41 |
50 |
51 | );
52 | };
--------------------------------------------------------------------------------
/lib/tests/hooks/useToggle.test.tsx:
--------------------------------------------------------------------------------
1 | import { act, renderHook } from '@testing-library/react';
2 | import { useBoolToggle, useToggle } from '../../hooks/useToggle';
3 |
4 | describe('useToggle', () => {
5 | it('should initialize with the initial value', () => {
6 | const { result } = renderHook(() => useToggle('on', ['on', 'off']));
7 |
8 | expect(result.current[0]).toBe('on');
9 | expect(typeof result.current[1]).toBe('function');
10 | });
11 |
12 | it('should toggle between options when called without arguments', () => {
13 | const { result } = renderHook(() => useToggle('on', ['on', 'off']));
14 |
15 | const toggleFn = result.current[1] as () => void;
16 |
17 | act(() => {
18 | toggleFn();
19 | });
20 |
21 | expect(result.current[0]).toBe('off');
22 |
23 | act(() => {
24 | toggleFn();
25 | });
26 |
27 | expect(result.current[0]).toBe('on');
28 | });
29 |
30 | it('should set specific value when called with argument', () => {
31 | const { result } = renderHook(() => useToggle('on', ['on', 'off']));
32 |
33 | const toggleFn = result.current[1] as (value: string) => void;
34 |
35 | act(() => {
36 | toggleFn('off');
37 | });
38 |
39 | expect(result.current[0]).toBe('off');
40 | });
41 |
42 | });
43 |
44 | describe('useBoolToggle', () => {
45 | it('should initialize with false by default', () => {
46 | const { result } = renderHook(() => useBoolToggle());
47 |
48 | expect(result.current[0]).toBe(false);
49 | });
50 |
51 | it('should initialize with provided value', () => {
52 | const { result } = renderHook(() => useBoolToggle(true));
53 |
54 | expect(result.current[0]).toBe(true);
55 | });
56 |
57 | it('should toggle between true and false', () => {
58 | const { result } = renderHook(() => useBoolToggle(false));
59 |
60 | const toggleFn = result.current[1] as () => void;
61 |
62 | act(() => {
63 | toggleFn();
64 | });
65 |
66 | expect(result.current[0]).toBe(true);
67 |
68 | act(() => {
69 | toggleFn();
70 | });
71 |
72 | expect(result.current[0]).toBe(false);
73 | });
74 | });
75 |
--------------------------------------------------------------------------------
/lib/hooks/useMutation.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 |
3 | const useMutation = (
4 | apiUrl: string,
5 | payload: any,
6 | headers: Record = {},
7 | options: { method?: string; timeout?: number } = {},
8 | onSuccess?: (data: any) => void,
9 | onError?: (error: string) => void
10 | ) => {
11 | const [loading, setLoading] = useState(false);
12 | const [response, setResponse] = useState(null);
13 | const [error, setError] = useState(null);
14 |
15 | useEffect(() => {
16 | let isMounted = true;
17 | const timeoutId = options.timeout ? setTimeout(() => {
18 | if (isMounted) {
19 | setLoading(false);
20 | setError('Request timed out');
21 | }
22 | }, options.timeout) : undefined;
23 |
24 | setLoading(true);
25 | setError(null);
26 |
27 | fetch(apiUrl, {
28 | method: options.method || 'POST',
29 | headers: {
30 | 'Content-Type': 'application/json',
31 | ...headers,
32 | },
33 | body: JSON.stringify(payload),
34 |
35 | })
36 | .then((response) => {
37 | clearTimeout(timeoutId);
38 | if (!response.ok) {
39 | throw new Error('Request failed');
40 | }
41 | return response.json();
42 | })
43 | .then((data) => {
44 | if (isMounted) {
45 | setLoading(false);
46 | setResponse(data);
47 | if (onSuccess) {
48 | onSuccess(data);
49 | }
50 | }
51 | })
52 | .catch((error) => {
53 | if (isMounted) {
54 | setLoading(false);
55 | setError(error.message || 'Something went wrong');
56 | if (onError) {
57 | onError(error.message || 'Something went wrong');
58 | }
59 | }
60 | });
61 |
62 | return () => {
63 | isMounted = false;
64 | if (timeoutId) {
65 | clearTimeout(timeoutId);
66 | }
67 | };
68 | }, [apiUrl, payload, headers, options.timeout, onSuccess, onError]);
69 |
70 | return { loading, response, error };
71 | };
72 |
73 | export default useMutation;
74 |
--------------------------------------------------------------------------------
/docs/demo/UseCookieListenerDemo.tsx:
--------------------------------------------------------------------------------
1 | import { useCookieListener } from 'react-haiku';
2 | import React, { useState } from 'react';
3 |
4 | export const UseCookieListenerDemo = () => {
5 | const [changes, setChanges] = useState([]);
6 | const [cookieName, setCookieName] = useState('demo_cookie');
7 | const [cookieValue, setCookieValue] = useState('');
8 |
9 | useCookieListener(
10 | (value, key) => {
11 | setChanges((prev) => [
12 | `[${new Date().toLocaleTimeString()}] ${key}=${value}`,
13 | ...prev,
14 | ]);
15 | },
16 | [cookieName],
17 | );
18 |
19 | return (
20 |
21 |
22 | setCookieName(e.target.value)}
29 | />
30 | setCookieValue(e.target.value)}
37 | />
38 | {
41 | document.cookie = `${cookieName}=${cookieValue}; path=/`;
42 | setCookieValue('');
43 | }}
44 | >
45 | Set Cookie
46 |
47 |
48 |
49 |
50 |
Change Log (last 5):
51 | {changes.slice(0, 5).map((change, i) => (
52 |
53 | {change}
54 |
55 | ))}
56 |
57 |
58 |
66 | Try changing cookies in another tab or DevTools
67 |
68 |
69 | );
70 | };
71 |
--------------------------------------------------------------------------------
/lib/tests/hooks/useNetwork.test.tsx:
--------------------------------------------------------------------------------
1 | import { renderHook, act } from '@testing-library/react';
2 | import { useNetwork } from '../../hooks/useNetwork';
3 | import { useEventListener } from '../../hooks/useEventListener';
4 |
5 | jest.mock('../../hooks/useEventListener', () => ({
6 | useEventListener: jest.fn(),
7 | }));
8 |
9 | describe('useNetwork', () => {
10 | const mockNavigator = { onLine: true };
11 |
12 | beforeAll(() => {
13 | Object.defineProperty(globalThis, 'navigator', {
14 | value: mockNavigator,
15 | writable: true,
16 | });
17 | });
18 |
19 | afterEach(() => {
20 | jest.resetAllMocks();
21 | });
22 |
23 | it('should return true when navigator.onLine is true', () => {
24 | mockNavigator.onLine = true;
25 |
26 | const { result } = renderHook(() => useNetwork());
27 |
28 | expect(result.current).toBe(true);
29 | });
30 |
31 | it('should add event listeners for online and offline', () => {
32 | renderHook(() => useNetwork());
33 |
34 | expect(useEventListener).toHaveBeenCalledWith('online', expect.any(Function));
35 | expect(useEventListener).toHaveBeenCalledWith('offline', expect.any(Function));
36 | });
37 |
38 | it('should default to true when navigator is undefined', () => {
39 | Object.defineProperty(globalThis, 'navigator', {
40 | value: undefined,
41 | writable: true,
42 | });
43 |
44 | const { result } = renderHook(() => useNetwork());
45 |
46 | expect(result.current).toBe(true);
47 | });
48 |
49 | it ('should return false when handleOffline is called', () => {
50 | const { result } = renderHook(() => useNetwork());
51 |
52 | const handleOffline = (useEventListener as jest.Mock).mock.calls[1][1];
53 |
54 | act(() => {
55 | handleOffline();
56 | });
57 |
58 | expect(result.current).toBe(false);
59 | });
60 | it('should return true when handleOnline is called', () => {
61 | const { result } = renderHook(() => useNetwork());
62 |
63 | const handleOnline = (useEventListener as jest.Mock).mock.calls[0][1];
64 |
65 | act(() => {
66 | handleOnline();
67 | });
68 |
69 | expect(result.current).toBe(true);
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/docs/demo/ImageDemo.tsx:
--------------------------------------------------------------------------------
1 | import { Image } from 'react-haiku';
2 | import React, { useState } from 'react';
3 |
4 | export const UseImageDemo = () => {
5 | const [customSrc, setCustomSrc] = useState('');
6 | const [showBroken, setShowBroken] = useState(false);
7 |
8 | return (
9 |
10 |
11 | setCustomSrc(e.target.value)}
18 | />
19 | setShowBroken(false)}
22 | >
23 | Load Custom Image
24 |
25 | setShowBroken(true)}
28 | >
29 | Load Broken Image
30 |
31 |
32 |
33 |
34 |
35 |
Original Image
36 |
46 |
47 |
48 |
49 |
Fallback Shown
50 |
55 |
56 |
57 |
58 |
66 | Try invalid URLs or click "Load Broken Image" to test fallback
67 |
68 |
69 | );
70 | };
71 |
--------------------------------------------------------------------------------
/lib/tests/hooks/useConfirmExit.test.tsx:
--------------------------------------------------------------------------------
1 | import { renderHook } from '@testing-library/react';
2 | import { useConfirmExit } from '../../hooks/useConfirmExit';
3 | import { on, off } from '../../helpers/event';
4 |
5 | jest.mock('../../helpers/event')
6 |
7 | const mockOn = on as jest.MockedFunction;
8 | const mockOff = off as jest.MockedFunction;
9 |
10 | const event = new Event('beforeunload');
11 | Object.defineProperty(event, 'preventDefault', { value: jest.fn() });
12 |
13 | describe('useConfirmExit', () => {
14 | beforeEach(() => {
15 | jest.clearAllMocks();
16 | });
17 |
18 | it('should add event listener when enabled is true', () => {
19 | renderHook(() => useConfirmExit(true));
20 |
21 | expect(mockOn).toHaveBeenCalledWith(window, 'beforeunload', expect.any(Function));
22 | });
23 |
24 | it('should not add event listener when enabled is false', () => {
25 | renderHook(() => useConfirmExit(false));
26 |
27 | expect(mockOn).not.toHaveBeenCalled();
28 | });
29 |
30 | it('should add event listener when enabled function returns true', () => {
31 | const enabledFn = jest.fn().mockReturnValue(true);
32 | renderHook(() => useConfirmExit(enabledFn));
33 |
34 | expect(on).toHaveBeenCalledWith(window, 'beforeunload', expect.any(Function));
35 | });
36 |
37 | it('should remove event listener on cleanup', () => {
38 | const { unmount } = renderHook(() => useConfirmExit(true));
39 |
40 | unmount();
41 |
42 | expect(mockOff).toHaveBeenCalledWith(window, 'beforeunload', expect.any(Function));
43 | });
44 |
45 | it('should call preventDefault', () => {
46 | renderHook(() => useConfirmExit(true, 'Custom message'));
47 |
48 | // Get the handler function.
49 | const handler = mockOn.mock.calls[0]?.[2];
50 |
51 | handler(event);
52 |
53 | expect(event.preventDefault).toHaveBeenCalled();
54 | });
55 |
56 | it('should not call preventDefault when enabled is false', () => {
57 | renderHook(() => useConfirmExit(() => false, 'Custom message'));
58 |
59 | // Get the handler function.
60 | const handler = mockOn.mock.calls[0]?.[2];
61 |
62 | handler(event);
63 |
64 | expect(event.preventDefault).not.toHaveBeenCalled();
65 | });
66 | });
67 |
--------------------------------------------------------------------------------
/docs/docs/intro.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 1
3 | ---
4 |
5 | import useBaseUrl from '@docusaurus/useBaseUrl';
6 |
7 | # Introduction
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
An awesome collection of React Hooks & Utilities!
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | [](https://github.com/DavidHDev/react-haiku/blob/main/LICENSE.md)
24 | [](https://www.npmjs.com/package/react-haiku)
25 | [](https://www.npmjs.com/package/react-haiku)
26 |
27 | ## What is this?
28 |
29 | Haiku is a `simple` & `lightweight` React library with the goal of saving
30 | you time by offering a large collection of `hooks` & `utilities` that will
31 | help you get the job done faster & more efficiently!
32 |
33 |
34 |
35 |
36 |
37 | ## Install
38 |
39 | Installing Haiku is very easy!
40 | *Requires React >=16.8.0*
41 |
42 | #### NPM
43 | ```sh
44 | npm install react-haiku
45 | ```
46 |
47 | #### Yarn
48 | ```sh
49 | yarn add react-haiku
50 | ```
51 | #### PNPM
52 | ```sh
53 | pnpm install react-haiku
54 | ```
55 |
56 |
57 |
58 | ## Using Haiku with NextJS
59 |
60 | Because Haiku uses ES6 modules, it is not transpiled by NextJS automatically since it's an external dependency, so you may have to follow a few extra steps to set it up correctly:
61 |
62 | _NOTE: These steps should no longer be required in Next 14 projects._
63 |
64 | 1. Add the `next-transpile-modules` package to your project:
65 |
66 | ```sh
67 | npm install next-transpile-modules
68 | ```
69 |
70 | 2. Configure your project's `next.config.js` file to transpile Haiku:
71 |
72 | ```js
73 | const withTM = require('next-transpile-modules')(['react-haiku']);
74 | module.exports = withTM({});
75 | ```
76 |
77 | After following these two steps, importing features from `react-haiku` into your project should work as expected!
78 |
79 |
80 | I hope you will have as much fun using Haiku as I had creating it!
81 |
82 |
--------------------------------------------------------------------------------