13 | }
14 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useCopyToClipboard/useCopyToClipboard.md:
--------------------------------------------------------------------------------
1 | React Hook for easy clipboard copy functionality.
2 |
3 | This hook provides a simple method to copy a string to the [clipboard](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/clipboard) and keeps track of the copied value. If the copying process encounters an issue, it logs a warning in the console, and the copied value remains null.
4 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useHover/useHover.demo.tsx:
--------------------------------------------------------------------------------
1 | import { useRef } from 'react'
2 |
3 | import { useHover } from './useHover'
4 |
5 | export default function Component() {
6 | const hoverRef = useRef(null)
7 | const isHover = useHover(hoverRef)
8 |
9 | return (
10 |
11 | {`The current div is ${isHover ? `hovered` : `unhovered`}`}
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useWindowSize/useWindowSize.md:
--------------------------------------------------------------------------------
1 | Easily retrieve window dimensions with this React Hook which also works onResize.
2 |
3 | ### Parameters
4 |
5 | - `initializeWithValue?: boolean`: If you use this hook in an SSR context, set it to `false` (default `true`)
6 | - `debounceDelay?: number`: The delay in milliseconds before the callback is invoked (disabled by default for retro-compatibility).
7 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useTimeout/useTimeout.md:
--------------------------------------------------------------------------------
1 | Very similar to the [`useInterval` ](/react-hook/use-interval) hook, this React hook implements the native [`setTimeout`](https://www.w3schools.com/jsref/met_win_settimeout.asp) function keeping the same interface.
2 |
3 | You can enable the timeout by setting `delay` as a `number` or disabling it using `null`.
4 |
5 | When the time finishes, the callback function is called.
6 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.demo.tsx:
--------------------------------------------------------------------------------
1 | import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'
2 |
3 | export default function Component() {
4 | useIsomorphicLayoutEffect(() => {
5 | console.log(
6 | "In the browser, I'm an `useLayoutEffect`, but in SSR, I'm an `useEffect`.",
7 | )
8 | }, [])
9 |
10 | return
Hello, world
11 | }
12 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useReadLocalStorage/useReadLocalStorage.test.ts:
--------------------------------------------------------------------------------
1 | import { renderHook } from '@testing-library/react'
2 |
3 | import { useReadLocalStorage } from './useReadLocalStorage'
4 |
5 | describe('useReadLocalStorage()', () => {
6 | it('should use read local storage', () => {
7 | const { result } = renderHook(() => useReadLocalStorage('test'))
8 |
9 | expect(result.current).toBe(null)
10 | })
11 | })
12 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useScreen/useScreen.md:
--------------------------------------------------------------------------------
1 | Easily retrieve `window.screen` object with this Hook React which also works onResize.
2 |
3 | ### Parameters
4 |
5 | - `initializeWithValue?: boolean`: If you use this hook in an SSR context, set it to `false`, it will initialize with `undefined` (default `true`).
6 | - `debounceDelay?: number`: The delay in milliseconds before the callback is invoked (disabled by default for retro-compatibility).
7 |
--------------------------------------------------------------------------------
/apps/www/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import type { ClassValue } from 'clsx'
2 | import { clsx } from 'clsx'
3 | import { twMerge } from 'tailwind-merge'
4 |
5 | import type { BaseHook, NavItem } from '@/types'
6 |
7 | export function cn(...inputs: ClassValue[]) {
8 | return twMerge(clsx(inputs))
9 | }
10 |
11 | export function mapHookToNavLink(hook: BaseHook): NavItem {
12 | return {
13 | title: hook.name,
14 | href: hook.path,
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/turbo/generators/templates/hook/hook.test.ts.hbs:
--------------------------------------------------------------------------------
1 | import { renderHook } from '@testing-library/react'
2 |
3 | import { {{camelCase name}} } from './{{camelCase name}}'
4 |
5 | describe('{{camelCase name}}()', () => {
6 | it('should {{name}} be ok', () => {
7 | const { result } = renderHook(() => {{camelCase name}}())
8 | const [value, setNumber] = result.current
9 |
10 | expect(value).toBe(2)
11 | expect(typeof setNumber).toBe('function')
12 | })
13 | })
14 |
15 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useDarkMode/useDarkMode.md:
--------------------------------------------------------------------------------
1 | This React Hook offers you an interface to set, enable, disable, toggle and read the dark theme mode.
2 | The returned value (`isDarkMode`) is a boolean to let you be able to use with your logic.
3 |
4 | It uses internally [`useLocalStorage()`](/react-hook/use-local-storage) to persist the value and listens the OS color scheme preferences.
5 |
6 | **Note**: If you use this hook in an SSR context, set the `initializeWithValue` option to `false`.
7 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useDarkMode/useDarkMode.demo.tsx:
--------------------------------------------------------------------------------
1 | import { useDarkMode } from './useDarkMode'
2 |
3 | export default function Component() {
4 | const { isDarkMode, toggle, enable, disable } = useDarkMode()
5 |
6 | return (
7 |
8 |
Current theme: {isDarkMode ? 'dark' : 'light'}
9 |
10 |
11 |
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/apps/www/src/config/site.ts:
--------------------------------------------------------------------------------
1 | import type { SiteConfig } from '@/types'
2 |
3 | export const siteConfig: SiteConfig = {
4 | name: 'usehooks-ts',
5 | description: 'React hook library, ready to use, written in Typescript.',
6 | url: 'https://usehooks-ts.com',
7 | ogImage:
8 | 'https://via.placeholder.com/1200x630.png/007ACC/fff/?text=usehooks-ts',
9 | links: {
10 | github: 'https://github.com/juliencrn/usehooks-ts',
11 | npm: 'https://www.npmjs.com/package/usehooks-ts',
12 | },
13 | }
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | # Logs
4 | logs
5 | *.log
6 | npm-debug.log*
7 | yarn-debug.log*
8 | yarn-error.log*
9 | npm-debug.log*
10 | *.tsbuildinfo
11 |
12 | # Cache
13 | .npm
14 | .cache
15 | .eslintcache
16 | .turbo
17 |
18 | # Compiled stuff
19 | dist
20 | generated
21 |
22 | # Coverage
23 | coverage
24 |
25 | # dotenv environment variable files
26 | .env*
27 | !.env.example
28 |
29 | # Output of 'npm pack'
30 | *.tgz
31 |
32 | # Mac files
33 | .DS_Store
34 |
35 | # Local Netlify folder
36 | .netlify
37 |
--------------------------------------------------------------------------------
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 |
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useDebounceValue/useDebounceValue.demo.tsx:
--------------------------------------------------------------------------------
1 | import { useDebounceValue } from './useDebounceValue'
2 |
3 | export default function Component({ defaultValue = 'John' }) {
4 | const [debouncedValue, setValue] = useDebounceValue(defaultValue, 500)
5 |
6 | return (
7 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useMediaQuery/useMediaQuery.md:
--------------------------------------------------------------------------------
1 | Easily retrieve media dimensions with this Hook React which also works onResize.
2 |
3 | **Note:**
4 |
5 | - If you use this hook in an SSR context, set the `initializeWithValue` option to `false`.
6 | - Before Safari 14, `MediaQueryList` is based on `EventTarget` and only supports `addListener`/`removeListener` for media queries. If you don't support these versions you may remove these checks. Read more about this on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/MediaQueryList/addListener).
7 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useReadLocalStorage/useReadLocalStorage.md:
--------------------------------------------------------------------------------
1 | This React Hook allows you to read a value from localStorage by its key. It can be useful if you just want to read without passing a default value.
2 | If the window object is not present (as in SSR), or if the value doesn't exist, `useReadLocalStorage()` will return `null`.
3 |
4 | **Note:**
5 |
6 | - If you use this hook in an SSR context, set the `initializeWithValue` option to `false`.
7 | - If you want to be able to change the value, see [useLocalStorage()](/react-hook/use-local-storage).
8 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useTimeout/useTimeout.demo.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 |
3 | import { useTimeout } from './useTimeout'
4 |
5 | export default function Component() {
6 | const [visible, setVisible] = useState(true)
7 |
8 | const hide = () => {
9 | setVisible(false)
10 | }
11 |
12 | useTimeout(hide, 5000)
13 |
14 | return (
15 |
16 |
17 | {visible
18 | ? "I'm visible for 5000ms"
19 | : 'You can no longer see this content'}
20 |
13 |
14 |
15 |
16 |
17 | >
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/apps/www/src/components/docs/right-sidebar.tsx:
--------------------------------------------------------------------------------
1 | import { CarbonAds } from '../carbon-ads'
2 | import type { TableOfContents } from './table-of-content'
3 | import { TableOfContent } from './table-of-content'
4 |
5 | type Props = {
6 | toc: TableOfContents
7 | }
8 |
9 | export function RightSidebar({ toc }: Props) {
10 | return (
11 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useInterval/useInterval.md:
--------------------------------------------------------------------------------
1 | Use [`setInterval`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval) in functional React component with the same API.
2 | Set your callback function as a first parameter and a delay (in milliseconds) for the second argument. You can also stop the timer passing `null` instead the delay or even, execute it right away passing `0`.
3 |
4 | The main difference between the `setInterval` you know and this `useInterval` hook is that its arguments are "dynamic". You can get more information in the Dan Abramov's [blog post](https://overreacted.io/making-setinterval-declarative-with-react-hooks/).
5 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useCountdown/useCountdown.md:
--------------------------------------------------------------------------------
1 | **IMPORTANT**: The new useCountdown is deprecating the old one on the next major version.
2 |
3 | A simple countdown implementation. Support increment and decrement.
4 |
5 | NEW VERSION: A simple countdown implementation. Accepts `countStop`(new), `countStart` (was `seconds`), `intervalMs`(was `interval`) and `isIncrement` as keys of the call argument. Support increment and decrement. Will stop when at `countStop`.
6 |
7 | Related hooks:
8 |
9 | - [`useBoolean()`](/react-hook/use-boolean)
10 | - [`useToggle()`](/react-hook/use-toggle)
11 | - [`useCounter()`](/react-hook/use-counter)
12 | - [`useInterval()`](/react-hook/use-interval)
13 |
--------------------------------------------------------------------------------
/apps/www/src/components/docs/page-header.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils'
2 |
3 | type DocsPageHeaderProps = {
4 | heading: string
5 | text?: string
6 | } & React.HTMLAttributes
7 |
8 | export function PageHeader({
9 | heading,
10 | text,
11 | className,
12 | ...props
13 | }: DocsPageHeaderProps) {
14 | return (
15 | <>
16 |
16 |
17 |
18 |
19 |
20 | >
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: View documentation 📚
4 | url: https://usehooks-ts.com/
5 | about: Check out the official docs for answers to common questions.
6 | - name: Feature requests 💡
7 | url: https://github.com/juliencrn/usehooks-ts/discussions/categories/ideas
8 | about: Suggest a new React hook idea.
9 | - name: Questions 💭
10 | url: https://github.com/juliencrn/usehooks-ts/discussions/categories/help
11 | about: Need support with a React hook problem? Open up a help request.
12 | - name: Donate ❤️
13 | url: https://github.com/sponsors/juliencrn
14 | about: Love usehooks-ts? Show your support by donating today!
15 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/update-docs.yml:
--------------------------------------------------------------------------------
1 | name: Update docs ✍️
2 | description: >-
3 | Find a mistake in our documentation, or have a suggestion to improve them? Let us know here.
4 | labels:
5 | - documentation
6 | body:
7 | - type: textarea
8 | id: description
9 | attributes:
10 | label: Describe the problem
11 | description: A clear and concise description of what is wrong in the documentation or what you would like to improve. Please include URLs to the pages you're referring to.
12 | validations:
13 | required: true
14 | - type: textarea
15 | id: context
16 | attributes:
17 | label: Additional context
18 | description: Add any other context about the problem here.
19 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useSessionStorage/useSessionStorage.md:
--------------------------------------------------------------------------------
1 | Persist the state with session storage so that it remains after a page refresh. This can be useful to record session information. This hook is used in the same way as useState except that you must pass the storage key in the 1st parameter. If the window object is not present (as in SSR), `useSessionStorage()` will return the default value.
2 |
3 | You can also pass an optional third parameter to use a custom serializer/deserializer.
4 |
5 | **Note**: If you use this hook in an SSR context, set the `initializeWithValue` option to `false`, it will initialize in SSR with the initial value.
6 |
7 | Related hooks:
8 |
9 | - [`useLocalStorage()`](/react-hook/use-local-storage)
10 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useOnClickOutside/useOnClickOutside.demo.tsx:
--------------------------------------------------------------------------------
1 | import { useRef } from 'react'
2 |
3 | import { useOnClickOutside } from './useOnClickOutside'
4 |
5 | export default function Component() {
6 | const ref = useRef(null)
7 |
8 | const handleClickOutside = () => {
9 | // Your custom logic here
10 | console.log('clicked outside')
11 | }
12 |
13 | const handleClickInside = () => {
14 | // Your custom logic here
15 | console.log('clicked inside')
16 | }
17 |
18 | useOnClickOutside(ref, handleClickOutside)
19 |
20 | return (
21 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useIsClient/useIsClient.test.ts:
--------------------------------------------------------------------------------
1 | import { renderHook } from '@testing-library/react'
2 |
3 | import { useIsClient } from './useIsClient'
4 |
5 | describe('useIsClient()', () => {
6 | // TODO: currently don't know how to simulate hydration of hooks. @see https://github.com/testing-library/react-testing-library/issues/1120
7 | it.skip('should be false when rendering on the server', () => {
8 | const { result } = renderHook(() => useIsClient(), { hydrate: false })
9 | expect(result.current).toBe(false)
10 | })
11 |
12 | it('should be true when after hydration', () => {
13 | const { result } = renderHook(() => useIsClient(), { hydrate: true })
14 | expect(result.current).toBe(true)
15 | })
16 | })
17 |
--------------------------------------------------------------------------------
/packages/eslint-config-custom/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # eslint-config-custom
2 |
3 | ## 2.0.0
4 |
5 | ### Major Changes
6 |
7 | - a8e8968: Move the full workspace into ES Module
8 |
9 | ### Minor Changes
10 |
11 | - a8e8968: Prefer type over interface (#515)
12 |
13 | ## 1.2.0
14 |
15 | ### Minor Changes
16 |
17 | - b5b9e1f: chore: Updated dependencies
18 |
19 | ## 1.1.1
20 |
21 | ### Patch Changes
22 |
23 | - add1431: Upgrade dependencies
24 | - 0321342: Make Typescript and typescript-eslint stricter
25 | - a192167: Migrate from jest to vitest
26 |
27 | ## 1.1.0
28 |
29 | ### Minor Changes
30 |
31 | - 4b3ed4e: Prevent circular dependencies with Eslint
32 |
33 | ## 1.0.1
34 |
35 | ### Patch Changes
36 |
37 | - 7141d01: Upgrade internal dependencies
38 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useIsClient/useIsClient.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | /**
4 | * Custom hook that determines if the code is running on the client side (in the browser).
5 | * @returns {boolean} A boolean value indicating whether the code is running on the client side.
6 | * @public
7 | * @see [Documentation](https://usehooks-ts.com/react-hook/use-is-client)
8 | * @example
9 | * ```tsx
10 | * const isClient = useIsClient();
11 | * // Use isClient to conditionally render or execute code specific to the client side.
12 | * ```
13 | */
14 | export function useIsClient() {
15 | const [isClient, setClient] = useState(false)
16 |
17 | useEffect(() => {
18 | setClient(true)
19 | }, [])
20 |
21 | return isClient
22 | }
23 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useDebounceCallback/useDebounceCallback.md:
--------------------------------------------------------------------------------
1 | Creates a debounced version of a callback function.
2 |
3 | ### Parameters
4 |
5 | - `func`: The callback function to be debounced.
6 | - `delay` (optional): The delay in milliseconds before the callback is invoked (default is 500 milliseconds).
7 | - `options` (optional): Options to control the behavior of the debounced function.
8 |
9 | ### Returns
10 |
11 | A debounced version of the original callback along with control functions.
12 |
13 | ### Dependency
14 |
15 | This hook is built upon [`lodash.debounce`](https://www.npmjs.com/package/lodash.debounce).
16 |
17 | ### Related hooks
18 |
19 | - [`useDebounceValue`](/react-hook/use-debounce-value): Built on top of `useDebounceCallback`, it returns the debounce value instead
20 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useLocalStorage/useLocalStorage.demo.tsx:
--------------------------------------------------------------------------------
1 | import { useLocalStorage } from './useLocalStorage'
2 |
3 | export default function Component() {
4 | const [value, setValue, removeValue] = useLocalStorage('test-key', 0)
5 |
6 | return (
7 |
8 |
Count: {value}
9 |
16 |
23 |
30 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useMap/useMap.md:
--------------------------------------------------------------------------------
1 | This React hook provides an API to interact with a `Map` ([Documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map))
2 |
3 | It takes as initial entries a `Map` or an array like `[["key": "value"], [..]]` or nothing and returns:
4 |
5 | - An array with an instance of `Map` (including: `foreach, get, has, entries, keys, values, size`)
6 | - And an object of methods (`set, setAll, remove, reset`)
7 |
8 | Make sure to use these methods to update the map, a `map.set(..)` would not re-render the component.
9 |
10 |
11 |
12 | **Why use Map instead of an object ?**
13 |
14 | Map is an iterable, a simple hash and it performs better in storing large data ([Read more](https://azimi.io/es6-map-with-react-usestate-9175cd7b409b)).
15 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useClickAnyWhere/useClickAnyWhere.ts:
--------------------------------------------------------------------------------
1 | import { useEventListener } from '../useEventListener'
2 |
3 | /**
4 | * Custom hook that handles click events anywhere on the document.
5 | * @param {Function} handler - The function to be called when a click event is detected anywhere on the document.
6 | * @public
7 | * @see [Documentation](https://usehooks-ts.com/react-hook/use-click-any-where)
8 | * @example
9 | * ```tsx
10 | * const handleClick = (event) => {
11 | * console.log('Document clicked!', event);
12 | * };
13 | *
14 | * // Attach click event handler to document
15 | * useClickAnywhere(handleClick);
16 | * ```
17 | */
18 | export function useClickAnyWhere(handler: (event: MouseEvent) => void) {
19 | useEventListener('click', event => {
20 | handler(event)
21 | })
22 | }
23 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useEventListener/useEventListener.md:
--------------------------------------------------------------------------------
1 | Use EventListener with simplicity by React Hook.
2 |
3 | Supports `Window`, `Element` and `Document` and custom events with almost the same parameters as the native [`addEventListener` options](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#syntax). See examples below.
4 |
5 | If you want to use your CustomEvent using Typescript, you have to declare the event type.
6 | Find which kind of Event you want to extends:
7 |
8 | - `MediaQueryListEventMap`
9 | - `WindowEventMap`
10 | - `HTMLElementEventMap`
11 | - `DocumentEventMap`
12 |
13 | Then declare your custom event:
14 |
15 | ```ts
16 | declare global {
17 | interface DocumentEventMap {
18 | 'my-custom-event': CustomEvent<{ exampleArg: string }>
19 | }
20 | }
21 | ```
22 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useSessionStorage/useSessionStorage.demo.tsx:
--------------------------------------------------------------------------------
1 | import { useSessionStorage } from './useSessionStorage'
2 |
3 | export default function Component() {
4 | const [value, setValue, removeValue] = useSessionStorage('test-key', 0)
5 |
6 | return (
7 |
Copied value: {copiedText ?? 'Nothing is copied yet!'}
25 | >
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useIsMounted/useIsMounted.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useRef } from 'react'
2 |
3 | /**
4 | * Custom hook that determines if the component is currently mounted.
5 | * @returns {() => boolean} A function that returns a boolean value indicating whether the component is mounted.
6 | * @public
7 | * @see [Documentation](https://usehooks-ts.com/react-hook/use-is-mounted)
8 | * @example
9 | * ```tsx
10 | * const isComponentMounted = useIsMounted();
11 | * // Use isComponentMounted() to check if the component is currently mounted before performing certain actions.
12 | * ```
13 | */
14 | export function useIsMounted(): () => boolean {
15 | const isMounted = useRef(false)
16 |
17 | useEffect(() => {
18 | isMounted.current = true
19 |
20 | return () => {
21 | isMounted.current = false
22 | }
23 | }, [])
24 |
25 | return useCallback(() => isMounted.current, [])
26 | }
27 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useStep/useStep.demo.tsx:
--------------------------------------------------------------------------------
1 | import { useStep } from './useStep'
2 |
3 | export default function Component() {
4 | const [currentStep, helpers] = useStep(5)
5 |
6 | const {
7 | canGoToPrevStep,
8 | canGoToNextStep,
9 | goToNextStep,
10 | goToPrevStep,
11 | reset,
12 | setStep,
13 | } = helpers
14 |
15 | return (
16 | <>
17 |
Current step is {currentStep}
18 |
Can go to previous step {canGoToPrevStep ? 'yes' : 'no'}
19 |
Can go to next step {canGoToNextStep ? 'yes' : 'no'}
20 |
21 |
22 |
23 |
30 | >
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/apps/www/env.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unsafe-call */
2 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */
3 | import { createEnv } from '@t3-oss/env-nextjs'
4 | import { z } from 'zod'
5 |
6 | export const env = createEnv({
7 | server: {},
8 | client: {
9 | NEXT_PUBLIC_GA_MEASUREMENT_ID: z.string().optional().default(''),
10 | NEXT_PUBLIC_ALGOLIA_APP_ID: z.string().optional().default(''),
11 | NEXT_PUBLIC_ALGOLIA_SEARCH_KEY: z.string().optional().default(''),
12 | NEXT_PUBLIC_UMAMI_WEBSITE_ID: z.string().optional().default(''),
13 | },
14 | runtimeEnv: {
15 | NEXT_PUBLIC_GA_MEASUREMENT_ID: process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID,
16 | NEXT_PUBLIC_ALGOLIA_APP_ID: process.env.NEXT_PUBLIC_ALGOLIA_APP_ID,
17 | NEXT_PUBLIC_ALGOLIA_SEARCH_KEY: process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY,
18 | NEXT_PUBLIC_UMAMI_WEBSITE_ID: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
19 | },
20 | })
21 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.md:
--------------------------------------------------------------------------------
1 | The React documentation says about `useLayoutEffect`:
2 |
3 | > The signature is identical to useEffect, but it fires synchronously after all DOM mutations.
4 |
5 | That means this hook is a browser hook. But React code could be generated from the server without the Window API.
6 |
7 | If you're using NextJS, you'll have this error message:
8 |
9 | > Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://reactjs.org/link/uselayouteffect-ssr for common fixes.
10 |
11 | This hook fixes this problem by switching between `useEffect` and `useLayoutEffect` following the execution environment.
12 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [juliencrn]
4 | # patreon: # Replace with a single Patreon username
5 | # open_collective: #usehooks-ts # Replace with a single Open Collective username
6 | # ko_fi: # Replace with a single Ko-fi username
7 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | # liberapay: # Replace with a single Liberapay username
10 | # issuehunt: # Replace with a single IssueHunt username
11 | # otechie: # Replace with a single Otechie username
12 | # lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | custom: [
14 | "https://juliencaron.com/donate",
15 | "https://www.paypal.com/paypalme/juliencrn",
16 | "https://buy.stripe.com/fZefZY8Bv32cg9O3cc",
17 | "https://www.buymeacoffee.com/juliencrn"
18 | ]
19 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useIntersectionObserver/useIntersectionObserver.demo.tsx:
--------------------------------------------------------------------------------
1 | import { useIntersectionObserver } from './useIntersectionObserver'
2 |
3 | const Section = (props: { title: string }) => {
4 | const { isIntersecting, ref } = useIntersectionObserver({
5 | threshold: 0.5,
6 | })
7 |
8 | console.log(`Render Section ${props.title}`, {
9 | isIntersecting,
10 | })
11 |
12 | return (
13 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/apps/www/src/components/carbon-ads/use-script.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react'
2 |
3 | // TODO: We can't use usehooks-ts's useScript because it mounts the script in the document.body, maybe provide a way to specify the mount point
4 | export const useScript = (scriptUrl: string, scriptId: string) => {
5 | const ref = useRef(null)
6 |
7 | useEffect(() => {
8 | const existingScript = document.getElementById(scriptId)
9 |
10 | if (!existingScript) {
11 | const script = document.createElement('script')
12 |
13 | script.setAttribute('async', '')
14 | script.setAttribute('type', 'text/javascript')
15 | script.setAttribute('src', scriptUrl)
16 | script.setAttribute('id', scriptId)
17 |
18 | ref.current?.appendChild(script)
19 | }
20 |
21 | return () => {
22 | if (existingScript) {
23 | existingScript.remove()
24 | }
25 | }
26 | }, [scriptUrl, scriptId])
27 |
28 | return ref
29 | }
30 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useIsMounted/useIsMounted.md:
--------------------------------------------------------------------------------
1 | In React, once a component is unmounted, it is deleted from memory and will never be mounted again. That's why we don't define a state in a disassembled component.
2 | Changing the state in an unmounted component will result this error:
3 |
4 | ```txt
5 | Warning: Can't perform a React state update on an unmounted component.
6 | This is a no-op, but it indicates a memory leak in your application.
7 | To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
8 | ```
9 |
10 | The right way to solve this is cleaning effect like the above message said.
11 | For example, see [`useInterval`](/react-hook/use-interval) or [`useEventListener`](/react-hook/use-event-listener).
12 |
13 | But, there are some cases like Promise or API calls where it's impossible to know if the component is still mounted at the resolve time.
14 |
15 | This React hook help you to avoid this error with a function that return a boolean, `isMounted`.
16 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useScrollLock/useScrollLock.demo.tsx:
--------------------------------------------------------------------------------
1 | import { useScrollLock } from './useScrollLock'
2 |
3 | // Example 1: Auto lock the scroll of the body element when the modal mounts
4 | export default function Modal() {
5 | useScrollLock()
6 | return
Modal
7 | }
8 |
9 | // Example 2: Manually lock and unlock the scroll for a specific target
10 | export function App() {
11 | const { lock, unlock } = useScrollLock({
12 | autoLock: false,
13 | lockTarget: '#scrollable',
14 | })
15 |
16 | return (
17 | <>
18 |
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useResizeObserver/useResizeObserver.md:
--------------------------------------------------------------------------------
1 | A React hook for observing the size of an element using the [ResizeObserver API](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver).
2 |
3 | ### Parameters
4 |
5 | - `ref`: The ref of the element to observe.
6 | - `onResize`: When using `onResize`, the hook doesn't re-render on element size changes; it delegates handling to the provided callback. (default is `undefined`).
7 | - `box`: The box model to use for the ResizeObserver. (default is `'content-box'`)
8 |
9 | ### Returns
10 |
11 | - An object with the `width` and `height` of the element if the `onResize` optional callback is not provided.
12 |
13 | ### Polyfill
14 |
15 | The `useResizeObserver` hook does not provide polyfill to give you control, but it's recommended. You can add it by re-exporting the hook like this:
16 |
17 | ```ts
18 | // useResizeObserver.ts
19 | import { ResizeObserver } from '@juggle/resize-observer'
20 | import { useResizeObserver } from 'usehooks-ts'
21 |
22 | if (!window.ResizeObserver) {
23 | window.ResizeObserver = ResizeObserver
24 | }
25 |
26 | export { useResizeObserver }
27 | ```
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020 Julien CARON
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useHover/useHover.test.ts:
--------------------------------------------------------------------------------
1 | import { act, fireEvent, renderHook } from '@testing-library/react'
2 |
3 | import { useHover } from './useHover'
4 |
5 | describe('useHover()', () => {
6 | const el = {
7 | current: document.createElement('div'),
8 | }
9 |
10 | it('result must be initially false', () => {
11 | const { result } = renderHook(() => useHover(el))
12 | expect(result.current).toBe(false)
13 | })
14 |
15 | it('value must be true when firing hover action on element', () => {
16 | const { result } = renderHook(() => useHover(el))
17 |
18 | expect(result.current).toBe(false)
19 |
20 | act(() => void fireEvent.mouseEnter(el.current))
21 | expect(result.current).toBe(true)
22 | })
23 |
24 | it('value must turn back into false when firing mouseleave action on element', () => {
25 | const { result } = renderHook(() => useHover(el))
26 |
27 | expect(result.current).toBe(false)
28 |
29 | act(() => void fireEvent.mouseEnter(el.current))
30 | expect(result.current).toBe(true)
31 |
32 | act(() => void fireEvent.mouseLeave(el.current))
33 | expect(result.current).toBe(false)
34 | })
35 | })
36 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-report.yml:
--------------------------------------------------------------------------------
1 | name: 'Bug report 🐞'
2 | description: >-
3 | Create a report to help us improve
4 | title: '[BUG]'
5 | labels:
6 | - bug
7 | body:
8 | - type: textarea
9 | id: describe_bug
10 | attributes:
11 | label: Describe the bug
12 | description: A clear and concise description of what the bug is. Include any error messages or unexpected behaviors.
13 | validations:
14 | required: true
15 |
16 | - type: textarea
17 | id: reproduce_steps
18 | attributes:
19 | label: To Reproduce
20 | description: Outline the steps to reproduce the issue. Be specific and include details like browser, OS, or any relevant configurations.
21 | validations:
22 | required: true
23 |
24 | - type: textarea
25 | id: expected_behavior
26 | attributes:
27 | label: Expected behavior
28 | description: Explain what you expected to happen.
29 | validations:
30 | required: true
31 |
32 | - type: textarea
33 | id: additional_context
34 | attributes:
35 | label: Additional context
36 | description: Include any other relevant information that might help understand the issue.
37 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx:
--------------------------------------------------------------------------------
1 | import { useRef } from 'react'
2 |
3 | import { useEventListener } from './useEventListener'
4 |
5 | export default function Component() {
6 | // Define button ref
7 | const buttonRef = useRef(null)
8 | const documentRef = useRef(document)
9 |
10 | const onScroll = (event: Event) => {
11 | console.log('window scrolled!', event)
12 | }
13 |
14 | const onClick = (event: Event) => {
15 | console.log('button clicked!', event)
16 | }
17 |
18 | const onVisibilityChange = (event: Event) => {
19 | console.log('doc visibility changed!', {
20 | isVisible: !document.hidden,
21 | event,
22 | })
23 | }
24 |
25 | // example with window based event
26 | useEventListener('scroll', onScroll)
27 |
28 | // example with document based event
29 | useEventListener('visibilitychange', onVisibilityChange, documentRef)
30 |
31 | // example with element based event
32 | useEventListener('click', onClick, buttonRef)
33 |
34 | return (
35 |
36 |
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useDebounceValue/useDebounceValue.md:
--------------------------------------------------------------------------------
1 | Returns a debounced version of the provided value, along with a function to update it.
2 |
3 | ### Parameters
4 |
5 | - `value`: The value to be debounced.
6 | - `delay`: The delay in milliseconds before the value is updated.
7 | - `options` (optional): Optional configurations for the debouncing behavior.
8 | - `leading` (optional): Determines if the debounced function should be invoked on the leading edge of the timeout.
9 | - `trailing` (optional): Determines if the debounced function should be invoked on the trailing edge of the timeout.
10 | - `maxWait` (optional): The maximum time the debounced function is allowed to be delayed before it's invoked.
11 | - `equalityFn` (optional): A custom equality function to compare the current and previous values.
12 |
13 | ### Returns
14 |
15 | An array containing the debounced value and the function to update it.
16 |
17 | ### Dependency
18 |
19 | This hook is built upon [`lodash.debounce`](https://www.npmjs.com/package/lodash.debounce).
20 |
21 | ### Related hooks
22 |
23 | - [`useDebounceCallback`](/react-hook/use-debounce-callback): `useDebounceValue` is built on top of `useDebounceCallback`, it gives more control.
24 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useDocumentTitle/useDocumentTitle.test.ts:
--------------------------------------------------------------------------------
1 | import { renderHook } from '@testing-library/react'
2 |
3 | import { useDocumentTitle } from './useDocumentTitle'
4 |
5 | describe('useDocumentTitle()', () => {
6 | it('title should be in the document', () => {
7 | renderHook(() => {
8 | useDocumentTitle('foo')
9 | })
10 | expect(window.document.title).toEqual('foo')
11 | })
12 |
13 | it('should unset title on unmount with `preserveTitleOnUnmount` options to `false`', () => {
14 | window.document.title = 'initial'
15 | const { unmount } = renderHook(() => {
16 | useDocumentTitle('updated', { preserveTitleOnUnmount: false })
17 | })
18 | expect(window.document.title).toEqual('updated')
19 | unmount()
20 | expect(window.document.title).toEqual('initial')
21 | })
22 |
23 | it("shouldn't unset title on unmount with `preserveTitleOnUnmount` options to `true` (default)", () => {
24 | window.document.title = 'initial'
25 | const { unmount } = renderHook(() => {
26 | useDocumentTitle('updated')
27 | })
28 | expect(window.document.title).toEqual('updated')
29 | unmount()
30 | expect(window.document.title).toEqual('updated')
31 | })
32 | })
33 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useBoolean'
2 | export * from './useClickAnyWhere'
3 | export * from './useCopyToClipboard'
4 | export * from './useCountdown'
5 | export * from './useCounter'
6 | export * from './useDarkMode'
7 | export * from './useDebounceCallback'
8 | export * from './useDebounceValue'
9 | export * from './useDocumentTitle'
10 | export * from './useEventCallback'
11 | export * from './useEventListener'
12 | export * from './useHover'
13 | export * from './useIntersectionObserver'
14 | export * from './useInterval'
15 | export * from './useIsClient'
16 | export * from './useIsMounted'
17 | export * from './useIsomorphicLayoutEffect'
18 | export * from './useLocalStorage'
19 | export * from './useMap'
20 | export * from './useMediaQuery'
21 | export * from './useOnClickOutside'
22 | export * from './useReadLocalStorage'
23 | export * from './useResizeObserver'
24 | export * from './useScreen'
25 | export * from './useScript'
26 | export * from './useScrollLock'
27 | export * from './useSessionStorage'
28 | export * from './useStep'
29 | export * from './useTernaryDarkMode'
30 | export * from './useTimeout'
31 | export * from './useToggle'
32 | export * from './useUnmount'
33 | export * from './useWindowSize'
34 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useTernaryDarkMode/useTernaryDarkMode.md:
--------------------------------------------------------------------------------
1 | This React Hook offers you an interface to toggle and read the dark theme mode between three values. It uses internally [`useLocalStorage()`](/react-hook/use-local-storage) to persist the value and listens the OS color scheme preferences.
2 |
3 | If no value exists in local storage, it will default to `"system"`, though this can be changed by using the `defaultValue` hook parameter.
4 |
5 | **Note**: If you use this hook in an SSR context, set the `initializeWithValue` option to `false`.
6 |
7 | Returned value
8 |
9 | - The `isDarkMode` is a boolean for the final outcome, to let you be able to use with your logic.
10 | - The `ternaryModeCode` is of a literal type `"dark" | "system" | "light"`.
11 |
12 | When `ternaryModeCode` is set to `system`, the `isDarkMode` will use system theme, like of iOS and MacOS.
13 |
14 | Also, `ternaryModeCode` implicitly exports a type with `type TernaryDarkMode = typeof ternaryDarkMode`
15 |
16 | Returned interface
17 |
18 | - The `toggleTernaryDarkMode` is a function to cycle `ternaryModeCode` between `dark`, `system` and `light`(in this order).
19 | - The `setTernaryDarkMode` accepts a parameter of type `TernaryDarkMode` and set it as `ternaryModeCode`.
20 |
--------------------------------------------------------------------------------
/.github/workflows/update-algolia-index.yml:
--------------------------------------------------------------------------------
1 | name: Update Algolia index
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | env:
9 | NODE_VERSION: 20
10 | PNPM_VERSION: 8
11 |
12 | jobs:
13 | run:
14 | runs-on: ubuntu-latest
15 | timeout-minutes: 15
16 | steps:
17 | - name: Checkout code
18 | uses: actions/checkout@v4
19 | with:
20 | fetch-depth: 2
21 |
22 | - name: Setup Pnpm
23 | uses: pnpm/action-setup@v3
24 | with:
25 | version: ${{ env.PNPM_VERSION }}
26 |
27 | - name: Setup Node.js environment
28 | uses: actions/setup-node@v4
29 | with:
30 | node-version: ${{ env.NODE_VERSION }}
31 | cache: 'pnpm'
32 | cache-dependency-path: '**/pnpm-lock.yaml'
33 |
34 | - name: Install dependencies
35 | run: pnpm install --frozen-lockfile
36 |
37 | - name: Build
38 | run: pnpm build
39 |
40 | - name: Generate documentation from JSDoc
41 | run: pnpm generate-doc
42 |
43 | - name: Update Algolia index
44 | env:
45 | ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
46 | ALGOLIA_ADMIN_KEY: ${{ secrets.ALGOLIA_ADMIN_KEY }}
47 | run: pnpm update-algolia-index
48 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useToggle/useToggle.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'react'
2 |
3 | import type { Dispatch, SetStateAction } from 'react'
4 |
5 | /**
6 | * Custom hook that manages a boolean toggle state in React components.
7 | * @param {boolean} [defaultValue] - The initial value for the toggle state.
8 | * @returns {[boolean, () => void, Dispatch>]} A tuple containing the current state,
9 | * a function to toggle the state, and a function to set the state explicitly.
10 | * @public
11 | * @see [Documentation](https://usehooks-ts.com/react-hook/use-toggle)
12 | * @example
13 | * ```tsx
14 | * const [isToggled, toggle, setToggle] = useToggle(); // Initial value is false
15 | * // OR
16 | * const [isToggled, toggle, setToggle] = useToggle(true); // Initial value is true
17 | * // Use isToggled in your component, toggle to switch the state, setToggle to set the state explicitly.
18 | * ```
19 | */
20 | export function useToggle(
21 | defaultValue?: boolean,
22 | ): [boolean, () => void, Dispatch>] {
23 | const [value, setValue] = useState(!!defaultValue)
24 |
25 | const toggle = useCallback(() => {
26 | setValue(x => !x)
27 | }, [])
28 |
29 | return [value, toggle, setValue]
30 | }
31 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useEventCallback/useEventCallback.md:
--------------------------------------------------------------------------------
1 | The `useEventCallback` hook is a utility for creating memoized event callback functions in React applications. It ensures that the provided callback function is memoized and stable across renders, while also preventing its invocation during the render phase.
2 |
3 | See: [How to read an often-changing value from useCallback?](https://legacy.reactjs.org/docs/hooks-faq.html#how-to-read-an-often-changing-value-from-usecallback)
4 |
5 | ### Parameters
6 |
7 | - `fn: (args) => result` - The callback function to be memoized.
8 |
9 | ### Return Value
10 |
11 | - `(args) => result` - A memoized event callback function.
12 |
13 | ### Features
14 |
15 | - **Memoization**: Optimizes performance by memoizing the callback function to avoid unnecessary re-renders.
16 | - **Prevents Invocation During Render**: Ensures the callback isn't called during rendering, preventing potential issues.
17 | - **Error Handling**: Throws an error if the callback is mistakenly invoked during rendering.
18 | - **Strict Mode Compatibility**: Works seamlessly with React's strict mode for better debugging and reliability.
19 |
20 | ### Notes
21 |
22 | Avoid using `useEventCallback` for callback functions that depend on frequently changing state or props.
23 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useInterval/useInterval.demo.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 |
3 | import type { ChangeEvent } from 'react'
4 |
5 | import { useInterval } from './useInterval'
6 |
7 | export default function Component() {
8 | // The counter
9 | const [count, setCount] = useState(0)
10 | // Dynamic delay
11 | const [delay, setDelay] = useState(1000)
12 | // ON/OFF
13 | const [isPlaying, setPlaying] = useState(false)
14 |
15 | useInterval(
16 | () => {
17 | // Your custom logic here
18 | setCount(count + 1)
19 | },
20 | // Delay in milliseconds or null to stop it
21 | isPlaying ? delay : null,
22 | )
23 |
24 | const handleChange = (event: ChangeEvent) => {
25 | setDelay(Number(event.target.value))
26 | }
27 |
28 | return (
29 | <>
30 |
75 | ) : null
76 | }
77 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useIntersectionObserver/useIntersectionObserver.md:
--------------------------------------------------------------------------------
1 | This React Hook detects visibility of a component on the viewport using the [`IntersectionObserver` API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) natively present in the browser.
2 |
3 | It can be very useful to lazy-loading of images, implementing "infinite scrolling", tracking view in GA or starting animations for example.
4 |
5 | ### Option properties
6 |
7 | - `threshold` (optional, default: `0`): A threshold indicating the percentage of the target's visibility needed to trigger the callback. Can be a single number or an array of numbers.
8 | - `root` (optional, default: `null`): The element that is used as the viewport for checking visibility of the target. It can be an Element, Document, or null.
9 | - `rootMargin` (optional, default: `'0%'`): A margin around the root. It specifies the size of the root's margin area.
10 | - `freezeOnceVisible` (optional, default: `false`): If true, freezes the intersection state once the element becomes visible. Once the element enters the viewport and triggers the callback, further changes in intersection will not update the state.
11 | - `onChange` (optional): A callback function to be invoked when the intersection state changes. It receives two parameters: `isIntersecting` (a boolean indicating if the element is intersecting) and `entry` (an IntersectionObserverEntry object representing the state of the intersection).
12 | - `initialIsIntersecting` (optional, default: `false`): The initial state of the intersection. If set to true, indicates that the element is intersecting initially.
13 |
14 | **Note:** This interface extends the native `IntersectionObserverInit` interface, which provides the base options for configuring the Intersection Observer.
15 |
16 | For more information on the Intersection Observer API and its options, refer to the [MDN Intersection Observer API documentation](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).
17 |
18 | ### Return
19 |
20 | The `IntersectionResult` type supports both array and object destructuring and includes the following properties:
21 |
22 | - `ref`: A function that can be used as a ref callback to set the target element.
23 | - `isIntersecting`: A boolean indicating if the target element is intersecting with the viewport.
24 | - `entry`: An optional `IntersectionObserverEntry` object representing the state of the intersection.
25 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useScript/useScript.test.ts:
--------------------------------------------------------------------------------
1 | import { act, cleanup, renderHook } from '@testing-library/react'
2 |
3 | import { useScript } from './useScript'
4 |
5 | describe('useScript', () => {
6 | it('should handle script loading error', () => {
7 | const src = 'https://example.com/myscript.js'
8 |
9 | const { result } = renderHook(() => useScript(src))
10 |
11 | expect(result.current).toBe('loading')
12 |
13 | act(() => {
14 | // Simulate script error
15 | document
16 | .querySelector(`script[src="${src}"]`)
17 | ?.dispatchEvent(new Event('error'))
18 | })
19 |
20 | expect(result.current).toBe('error')
21 | })
22 |
23 | it('should remove script on unmount', () => {
24 | const src = '/'
25 |
26 | // First load the script
27 | const { result } = renderHook(() =>
28 | useScript(src, { removeOnUnmount: true }),
29 | )
30 |
31 | expect(result.current).toBe('loading')
32 |
33 | // Make sure the document is loaded
34 | act(() => {
35 | document
36 | .querySelector(`script[src="${src}"]`)
37 | ?.dispatchEvent(new Event('load'))
38 | })
39 |
40 | expect(result.current).toBe('ready')
41 |
42 | // Remove the hook by unmounting and cleaning up the hook
43 | cleanup()
44 |
45 | // Check if the script is removed from the DOM
46 | expect(document.querySelector(`script[src="${src}"]`)).toBeNull()
47 |
48 | // Try loading the script again
49 | const { result: result2 } = renderHook(() =>
50 | useScript(src, { removeOnUnmount: true }),
51 | )
52 |
53 | expect(result2.current).toBe('loading')
54 |
55 | // Make sure the document is loaded
56 | act(() => {
57 | document
58 | .querySelector(`script[src="${src}"]`)
59 | ?.dispatchEvent(new Event('load'))
60 | })
61 |
62 | expect(result2.current).toBe('ready')
63 | })
64 |
65 | it('should have a `id` attribute when given', () => {
66 | const src = '/'
67 | const id = 'my-script'
68 |
69 | const { result } = renderHook(() => useScript(src, { id }))
70 |
71 | // Make sure the document is loaded
72 | act(() => {
73 | document
74 | .querySelector(`script[src="${src}"]`)
75 | ?.dispatchEvent(new Event('load'))
76 | })
77 |
78 | expect(result.current).toBe('ready')
79 |
80 | expect(document.querySelector(`script[id="${id}"]`)).not.toBeNull()
81 | expect(document.querySelector(`script[src="${src}"]`)?.id).toBe(id)
82 | })
83 | })
84 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useWindowSize/useWindowSize.test.ts:
--------------------------------------------------------------------------------
1 | import { act, renderHook } from '@testing-library/react'
2 |
3 | import { useWindowSize } from './useWindowSize'
4 |
5 | const windowResize = (dimension: 'width' | 'height', value: number): void => {
6 | if (dimension === 'width') {
7 | window.innerWidth = value
8 | }
9 |
10 | if (dimension === 'height') {
11 | window.innerHeight = value
12 | }
13 |
14 | window.dispatchEvent(new Event('resize'))
15 | }
16 |
17 | describe('useWindowSize()', () => {
18 | beforeEach(() => {
19 | vi.clearAllMocks()
20 | vi.useFakeTimers() // Mock timers
21 |
22 | // Set the initial window size
23 | windowResize('width', 1920)
24 | windowResize('height', 1080)
25 | })
26 |
27 | it('should initialize', () => {
28 | const { result } = renderHook(() => useWindowSize())
29 | const { height, width } = result.current
30 | expect(typeof height).toBe('number')
31 | expect(typeof width).toBe('number')
32 | expect(result.current.width).toBe(1920)
33 | expect(result.current.height).toBe(1080)
34 | })
35 |
36 | it('should return the corresponding height', () => {
37 | const { result } = renderHook(() => useWindowSize())
38 |
39 | act(() => {
40 | windowResize('height', 420)
41 | })
42 |
43 | expect(result.current.height).toBe(420)
44 |
45 | act(() => {
46 | windowResize('height', 2196)
47 | })
48 |
49 | expect(result.current.height).toBe(2196)
50 | })
51 |
52 | it('should return the corresponding width', () => {
53 | const { result } = renderHook(() => useWindowSize())
54 |
55 | act(() => {
56 | windowResize('width', 420)
57 | })
58 |
59 | expect(result.current.width).toBe(420)
60 |
61 | act(() => {
62 | windowResize('width', 2196)
63 | })
64 |
65 | expect(result.current.width).toBe(2196)
66 | })
67 |
68 | it('should debounce the callback', () => {
69 | const { result } = renderHook(() => useWindowSize({ debounceDelay: 100 }))
70 |
71 | expect(result.current.width).toBe(1920)
72 | expect(result.current.height).toBe(1080)
73 |
74 | act(() => {
75 | windowResize('width', 2196)
76 | windowResize('height', 2196)
77 | })
78 |
79 | // Don't changed yet
80 | expect(result.current.width).toBe(1920)
81 | expect(result.current.height).toBe(1080)
82 |
83 | act(() => {
84 | vi.advanceTimersByTime(200)
85 | })
86 |
87 | expect(result.current.width).toBe(2196)
88 | expect(result.current.height).toBe(2196)
89 | })
90 | })
91 |
--------------------------------------------------------------------------------
/apps/www/tailwind.config.js:
--------------------------------------------------------------------------------
1 | import { fontFamily } from 'tailwindcss/defaultTheme'
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | const config = {
5 | content: ['./src/**/*.{ts,tsx,css}'],
6 | theme: {
7 | container: {
8 | center: true,
9 | padding: '2rem',
10 | screens: {
11 | '2xl': '1400px',
12 | },
13 | },
14 | extend: {
15 | colors: {
16 | border: 'hsl(var(--border))',
17 | input: 'hsl(var(--input))',
18 | ring: 'hsl(var(--ring))',
19 | background: 'hsl(var(--background))',
20 | foreground: 'hsl(var(--foreground))',
21 | primary: {
22 | DEFAULT: 'hsl(var(--primary))',
23 | foreground: 'hsl(var(--primary-foreground))',
24 | },
25 | secondary: {
26 | DEFAULT: 'hsl(var(--secondary))',
27 | foreground: 'hsl(var(--secondary-foreground))',
28 | },
29 | destructive: {
30 | DEFAULT: 'hsl(var(--destructive))',
31 | foreground: 'hsl(var(--destructive-foreground))',
32 | },
33 | muted: {
34 | DEFAULT: 'hsl(var(--muted))',
35 | foreground: 'hsl(var(--muted-foreground))',
36 | },
37 | accent: {
38 | DEFAULT: 'hsl(var(--accent))',
39 | foreground: 'hsl(var(--accent-foreground))',
40 | },
41 | popover: {
42 | DEFAULT: 'hsl(var(--popover))',
43 | foreground: 'hsl(var(--popover-foreground))',
44 | },
45 | card: {
46 | DEFAULT: 'hsl(var(--card))',
47 | foreground: 'hsl(var(--card-foreground))',
48 | },
49 | },
50 | borderRadius: {
51 | lg: 'var(--radius)',
52 | md: 'calc(var(--radius) - 2px)',
53 | sm: 'calc(var(--radius) - 4px)',
54 | },
55 | fontFamily: {
56 | sans: ['var(--font-sans)', ...fontFamily.sans],
57 | heading: ['var(--font-heading)', ...fontFamily.sans],
58 | },
59 | keyframes: {
60 | 'accordion-down': {
61 | from: { height: 0 },
62 | to: { height: 'var(--radix-accordion-content-height)' },
63 | },
64 | 'accordion-up': {
65 | from: { height: 'var(--radix-accordion-content-height)' },
66 | to: { height: 0 },
67 | },
68 | },
69 | animation: {
70 | 'accordion-down': 'accordion-down 0.2s ease-out',
71 | 'accordion-up': 'accordion-up 0.2s ease-out',
72 | },
73 | },
74 | },
75 | plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')],
76 | }
77 |
78 | export default config
79 |
--------------------------------------------------------------------------------
/apps/www/src/app/(docs)/introduction/page.tsx:
--------------------------------------------------------------------------------
1 | import { CommandCopy } from '@/components/command-copy'
2 | import { PageHeader } from '@/components/docs/page-header'
3 | import { RightSidebar } from '@/components/docs/right-sidebar'
4 | import { components } from '@/components/ui/components'
5 | import { siteConfig } from '@/config/site'
6 |
7 | export default async function IntroductionPage() {
8 | return (
9 |
10 |
11 |
16 | Introduction
17 |
18 | useHooks(🔥).ts
19 | is a React hooks library, written in Typescript and easy to use. It
20 | provides a set of hooks that enables you to build your React
21 | applications faster. The hooks are built upon the principles of{' '}
22 | DRY (Don't Repeat
23 | Yourself). There are hooks for most common use cases you might need.
24 |
25 |
26 | The library is designed to be as minimal as possible. It is fully
27 | tree-shakable (using the ESM version), meaning that you only import
28 | the hooks you need, and the rest will be removed from your bundle
29 | making the cost of using this library negligible. Most hooks are
30 | extensively tested and are being used in production environments.
31 |
32 | Install
33 |
34 | Get started installing{' '}
35 |
36 | usehooks-ts
37 | {' '}
38 | using your preferred package manager.
39 |
40 |
49 |
50 |
51 |
59 |
60 | )
61 | }
62 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useStep/useStep.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'react'
2 |
3 | import type { Dispatch, SetStateAction } from 'react'
4 |
5 | /** Represents the second element of the output of the `useStep` hook. */
6 | type UseStepActions = {
7 | /** Go to the next step in the process. */
8 | goToNextStep: () => void
9 | /** Go to the previous step in the process. */
10 | goToPrevStep: () => void
11 | /** Reset the step to the initial step. */
12 | reset: () => void
13 | /** Check if the next step is available. */
14 | canGoToNextStep: boolean
15 | /** Check if the previous step is available. */
16 | canGoToPrevStep: boolean
17 | /** Set the current step to a specific value. */
18 | setStep: Dispatch>
19 | }
20 |
21 | type SetStepCallbackType = (step: number | ((step: number) => number)) => void
22 |
23 | /**
24 | * Custom hook that manages and navigates between steps in a multi-step process.
25 | * @param {number} maxStep - The maximum step in the process.
26 | * @returns {[number, UseStepActions]} An tuple containing the current step and helper functions for navigating steps.
27 | * @public
28 | * @see [Documentation](https://usehooks-ts.com/react-hook/use-step)
29 | * @example
30 | * ```tsx
31 | * const [currentStep, { goToNextStep, goToPrevStep, reset, canGoToNextStep, canGoToPrevStep, setStep }] = useStep(3);
32 | * // Access and use the current step and provided helper functions.
33 | * ```
34 | */
35 | export function useStep(maxStep: number): [number, UseStepActions] {
36 | const [currentStep, setCurrentStep] = useState(1)
37 |
38 | const canGoToNextStep = currentStep + 1 <= maxStep
39 | const canGoToPrevStep = currentStep - 1 > 0
40 |
41 | const setStep = useCallback(
42 | step => {
43 | // Allow value to be a function so we have the same API as useState
44 | const newStep = step instanceof Function ? step(currentStep) : step
45 |
46 | if (newStep >= 1 && newStep <= maxStep) {
47 | setCurrentStep(newStep)
48 | return
49 | }
50 |
51 | throw new Error('Step not valid')
52 | },
53 | [maxStep, currentStep],
54 | )
55 |
56 | const goToNextStep = useCallback(() => {
57 | if (canGoToNextStep) {
58 | setCurrentStep(step => step + 1)
59 | }
60 | }, [canGoToNextStep])
61 |
62 | const goToPrevStep = useCallback(() => {
63 | if (canGoToPrevStep) {
64 | setCurrentStep(step => step - 1)
65 | }
66 | }, [canGoToPrevStep])
67 |
68 | const reset = useCallback(() => {
69 | setCurrentStep(1)
70 | }, [])
71 |
72 | return [
73 | currentStep,
74 | {
75 | goToNextStep,
76 | goToPrevStep,
77 | canGoToNextStep,
78 | canGoToPrevStep,
79 | setStep,
80 | reset,
81 | },
82 | ]
83 | }
84 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useDebounceValue/useDebounceValue.ts:
--------------------------------------------------------------------------------
1 | import { useRef, useState } from 'react'
2 |
3 | import type { DebouncedState } from '../useDebounceCallback'
4 | import { useDebounceCallback } from '../useDebounceCallback'
5 |
6 | /**
7 | * Hook options.
8 | * @template T - The type of the value.
9 | */
10 | type UseDebounceValueOptions = {
11 | /**
12 | * Determines whether the function should be invoked on the leading edge of the timeout.
13 | * @default false
14 | */
15 | leading?: boolean
16 | /**
17 | * Determines whether the function should be invoked on the trailing edge of the timeout.
18 | * @default false
19 | */
20 | trailing?: boolean
21 | /**
22 | * The maximum time the specified function is allowed to be delayed before it is invoked.
23 | */
24 | maxWait?: number
25 | /** A function to determine if the value has changed. Defaults to a function that checks if the value is strictly equal to the previous value. */
26 | equalityFn?: (left: T, right: T) => boolean
27 | }
28 |
29 | /**
30 | * Custom hook that returns a debounced version of the provided value, along with a function to update it.
31 | * @template T - The type of the value.
32 | * @param {T | (() => T)} initialValue - The value to be debounced.
33 | * @param {number} delay - The delay in milliseconds before the value is updated (default is 500ms).
34 | * @param {object} [options] - Optional configurations for the debouncing behavior.
35 | * @returns {[T, DebouncedState<(value: T) => void>]} An array containing the debounced value and the function to update it.
36 | * @public
37 | * @see [Documentation](https://usehooks-ts.com/react-hook/use-debounce-value)
38 | * @example
39 | * ```tsx
40 | * const [debouncedValue, updateDebouncedValue] = useDebounceValue(inputValue, 500, { leading: true });
41 | * ```
42 | */
43 | export function useDebounceValue(
44 | initialValue: T | (() => T),
45 | delay: number,
46 | options?: UseDebounceValueOptions,
47 | ): [T, DebouncedState<(value: T) => void>] {
48 | const eq = options?.equalityFn ?? ((left: T, right: T) => left === right)
49 | const unwrappedInitialValue =
50 | initialValue instanceof Function ? initialValue() : initialValue
51 | const [debouncedValue, setDebouncedValue] = useState(unwrappedInitialValue)
52 | const previousValueRef = useRef(unwrappedInitialValue)
53 |
54 | const updateDebouncedValue = useDebounceCallback(
55 | setDebouncedValue,
56 | delay,
57 | options,
58 | )
59 |
60 | // Update the debounced value if the initial value changes
61 | if (!eq(previousValueRef.current as T, unwrappedInitialValue)) {
62 | updateDebouncedValue(unwrappedInitialValue)
63 | previousValueRef.current = unwrappedInitialValue
64 | }
65 |
66 | return [debouncedValue, updateDebouncedValue]
67 | }
68 |
--------------------------------------------------------------------------------
/apps/www/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 | @import './prism.css';
5 |
6 | @layer base {
7 | :root {
8 | --background: 0 0% 100%;
9 | --foreground: 222.2 47.4% 11.2%;
10 |
11 | --muted: 210 40% 96.1%;
12 | --muted-foreground: 215.4 16.3% 46.9%;
13 |
14 | --popover: 0 0% 100%;
15 | --popover-foreground: 222.2 47.4% 11.2%;
16 |
17 | --card: 0 0% 100%;
18 | --card-foreground: 222.2 47.4% 11.2%;
19 |
20 | --border: 214.3 31.8% 91.4%;
21 | --input: 214.3 31.8% 91.4%;
22 |
23 | --primary: 222.2 47.4% 11.2%;
24 | --primary-foreground: 210 40% 98%;
25 |
26 | --secondary: 210 40% 96.1%;
27 | --secondary-foreground: 222.2 47.4% 11.2%;
28 |
29 | --accent: 210 40% 96.1%;
30 | --accent-foreground: 222.2 47.4% 11.2%;
31 |
32 | --destructive: 0 100% 50%;
33 | --destructive-foreground: 210 40% 98%;
34 |
35 | --ring: 215 20.2% 65.1%;
36 |
37 | --radius: 0.5rem;
38 | }
39 |
40 | @media (prefers-color-scheme: dark) {
41 | :root {
42 | --background: 224 71% 4%;
43 | --foreground: 213 31% 91%;
44 |
45 | --muted: 223 47% 11%;
46 | --muted-foreground: 215.4 16.3% 56.9%;
47 |
48 | --popover: 224 71% 4%;
49 | --popover-foreground: 215 20.2% 65.1%;
50 |
51 | --card: 224 71% 4%;
52 | --card-foreground: 213 31% 91%;
53 |
54 | --border: 216 34% 17%;
55 | --input: 216 34% 17%;
56 |
57 | --primary: 210 40% 98%;
58 | --primary-foreground: 222.2 47.4% 1.2%;
59 |
60 | --secondary: 222.2 47.4% 11.2%;
61 | --secondary-foreground: 210 40% 98%;
62 |
63 | --accent: 216 34% 17%;
64 | --accent-foreground: 210 40% 98%;
65 |
66 | --destructive: 0 63% 31%;
67 | --destructive-foreground: 210 40% 98%;
68 |
69 | --ring: 216 34% 17%;
70 |
71 | --radius: 0.5rem;
72 | }
73 | }
74 | }
75 |
76 | @layer base {
77 | * {
78 | @apply border-border;
79 | }
80 | html {
81 | scroll-behavior: smooth;
82 |
83 | color-scheme: light;
84 |
85 | @media (prefers-color-scheme: dark) {
86 | color-scheme: dark;
87 | }
88 | }
89 | body {
90 | @apply bg-background text-foreground;
91 | font-feature-settings:
92 | 'rlig' 1,
93 | 'calt' 1;
94 | }
95 |
96 | /* Override the default styles for the Carbon Ads block */
97 | .carbon-wrap * {
98 | --carbon-bg-primary: hsl(var(--background));
99 | --carbon-bg-secondary: hsl(var(--border));
100 | --carbon-text-color: hsl(var(--foreground));
101 | }
102 |
103 | .carbon-wrap .carbon-responsive-wrap {
104 | @apply !rounded-md;
105 | }
106 |
107 | .carbon-wrap .carbon-poweredby {
108 | @apply !opacity-100 !text-muted-foreground;
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useMediaQuery/useMediaQuery.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 |
3 | import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect'
4 |
5 | /** Hook options. */
6 | type UseMediaQueryOptions = {
7 | /**
8 | * The default value to return if the hook is being run on the server.
9 | * @default false
10 | */
11 | defaultValue?: boolean
12 | /**
13 | * If `true` (default), the hook will initialize reading the media query. In SSR, you should set it to `false`, returning `options.defaultValue` or `false` initially.
14 | * @default true
15 | */
16 | initializeWithValue?: boolean
17 | }
18 |
19 | const IS_SERVER = typeof window === 'undefined'
20 |
21 | /**
22 | * Custom hook that tracks the state of a media query using the [`Match Media API`](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia).
23 | * @param {string} query - The media query to track.
24 | * @param {?UseMediaQueryOptions} [options] - The options for customizing the behavior of the hook (optional).
25 | * @returns {boolean} The current state of the media query (true if the query matches, false otherwise).
26 | * @public
27 | * @see [Documentation](https://usehooks-ts.com/react-hook/use-media-query)
28 | * @example
29 | * ```tsx
30 | * const isSmallScreen = useMediaQuery('(max-width: 600px)');
31 | * // Use `isSmallScreen` to conditionally apply styles or logic based on the screen size.
32 | * ```
33 | */
34 | export function useMediaQuery(
35 | query: string,
36 | {
37 | defaultValue = false,
38 | initializeWithValue = true,
39 | }: UseMediaQueryOptions = {},
40 | ): boolean {
41 | const getMatches = (query: string): boolean => {
42 | if (IS_SERVER) {
43 | return defaultValue
44 | }
45 | return window.matchMedia(query).matches
46 | }
47 |
48 | const [matches, setMatches] = useState(() => {
49 | if (initializeWithValue) {
50 | return getMatches(query)
51 | }
52 | return defaultValue
53 | })
54 |
55 | // Handles the change event of the media query.
56 | function handleChange() {
57 | setMatches(getMatches(query))
58 | }
59 |
60 | useIsomorphicLayoutEffect(() => {
61 | const matchMedia = window.matchMedia(query)
62 |
63 | // Triggered at the first client-side load and if query changes
64 | handleChange()
65 |
66 | // Use deprecated `addListener` and `removeListener` to support Safari < 14 (#135)
67 | if (matchMedia.addListener) {
68 | matchMedia.addListener(handleChange)
69 | } else {
70 | matchMedia.addEventListener('change', handleChange)
71 | }
72 |
73 | return () => {
74 | if (matchMedia.removeListener) {
75 | matchMedia.removeListener(handleChange)
76 | } else {
77 | matchMedia.removeEventListener('change', handleChange)
78 | }
79 | }
80 | }, [query])
81 |
82 | return matches
83 | }
84 |
--------------------------------------------------------------------------------
/scripts/update-testing-issue.js:
--------------------------------------------------------------------------------
1 | import { path, fs, $ } from 'zx'
2 |
3 | import { getHooks } from './utils/get-hooks.js'
4 |
5 | const SOURCE_DIR = path.resolve('./packages/usehooks-ts/src')
6 | const GITHUB_REPO = `juliencrn/usehooks-ts`
7 | const GITHUB_ISSUE_PATH = `${GITHUB_REPO}/issues/423`
8 | const EXCLUDED_HOOK = ['useIsomorphicLayoutEffect']
9 |
10 | // Read hook list from the `generated/typedoc/all.json` file
11 | const hooks = getHooks()
12 | // Filter excluded hooks
13 | .filter(hook => !EXCLUDED_HOOK.includes(hook.name))
14 | // For each hook, check if there is a test file
15 | .map(hook => {
16 | const files = fs.readdirSync(path.resolve(SOURCE_DIR, hook.name))
17 | return { ...hook, hasTest: files.some(isTestFile) }
18 | })
19 | // Generate the markdown lines
20 | .map(hook => {
21 | const url = `https://github.com/${GITHUB_REPO}/tree/master/packages/usehooks-ts/src/${hook.name}`
22 | return {
23 | ...hook,
24 | markdown: `- [${hook.hasTest ? 'x' : ' '}] [\`${hook.name}\`](${url})`,
25 | }
26 | })
27 |
28 | // Compute the state of the issue
29 | const url = `https://github.com/${GITHUB_ISSUE_PATH}`
30 | const testedCount = hooks.filter(({ hasTest }) => hasTest).length
31 | const state = hooks.length === testedCount ? 'closed' : 'open'
32 | const body = hooks.map(({ markdown }) => markdown).join('\n')
33 |
34 | // Update the github testing issue
35 | await $`gh api \
36 | --method PATCH \
37 | -H "Accept: application/vnd.github+json" \
38 | -H "X-GitHub-Api-Version: 2022-11-28" \
39 | /repos/${GITHUB_ISSUE_PATH} \
40 | -f body=${issueTemplate(body)} \
41 | -f state=${state}
42 | `
43 |
44 | console.log(`\n\n✅ Issue successfully updated! -> ${url}`)
45 |
46 | // Utils
47 |
48 | function isTestFile(filename) {
49 | return /^use[A-Z][a-zA-Z]*.test.tsx?$/.test(filename)
50 | }
51 |
52 | function issueTemplate(body) {
53 | return `## Overview
54 |
55 | This GitHub issue serves as a central hub for the unit-testing journey of our React hook library. Our goal is to ensure robust and reliable testing for each individual hook in the library.
56 |
57 | ## Objectives
58 |
59 | 1. **Comprehensive Testing**: Write unit tests for each hook to ensure thorough coverage of functionality.
60 | 2. **Consistent Test Structure**: Maintain a consistent structure/format for unit tests across all hooks.
61 | 3. **Documentation**: Document the purpose and usage of each test to enhance overall project understanding.
62 |
63 | ## Getting Started
64 |
65 | 1. Fork the repository to your account.
66 | 2. Create a new branch for your tests: git checkout -b feature/hook-name-tests.
67 | 3. Write tests for the specific hook in \`packages/usehooks-ts/src/useExample/useExample.test.ts\`.
68 | 4. Ensure all tests pass before submitting a pull request.
69 |
70 | ## Hooks to Test
71 |
72 | ${body}
73 |
74 | Let's ensure our hooks are well-tested and reliable!`
75 | }
76 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useBoolean/useBoolean.test.ts:
--------------------------------------------------------------------------------
1 | import { act, renderHook } from '@testing-library/react'
2 |
3 | import { useBoolean } from './useBoolean'
4 |
5 | describe('useBoolean()', () => {
6 | it('should use boolean', () => {
7 | const { result } = renderHook(() => useBoolean())
8 |
9 | expect(result.current.value).toBe(false)
10 | expect(typeof result.current.setTrue).toBe('function')
11 | expect(typeof result.current.setFalse).toBe('function')
12 | expect(typeof result.current.toggle).toBe('function')
13 | expect(typeof result.current.setValue).toBe('function')
14 | })
15 |
16 | it('should default value works (1)', () => {
17 | const { result } = renderHook(() => useBoolean(true))
18 |
19 | expect(result.current.value).toBe(true)
20 | })
21 |
22 | it('should default value works (2)', () => {
23 | const { result } = renderHook(() => useBoolean(false))
24 |
25 | expect(result.current.value).toBe(false)
26 | })
27 |
28 | it('should set to true (1)', () => {
29 | const { result } = renderHook(() => useBoolean(false))
30 |
31 | act(() => {
32 | result.current.setTrue()
33 | })
34 |
35 | expect(result.current.value).toBe(true)
36 | })
37 |
38 | it('should set to true (2)', () => {
39 | const { result } = renderHook(() => useBoolean(false))
40 |
41 | act(() => {
42 | result.current.setTrue()
43 | result.current.setTrue()
44 | })
45 |
46 | expect(result.current.value).toBe(true)
47 | })
48 |
49 | it('should set to false (1)', () => {
50 | const { result } = renderHook(() => useBoolean(true))
51 |
52 | act(() => {
53 | result.current.setFalse()
54 | })
55 |
56 | expect(result.current.value).toBe(false)
57 | })
58 |
59 | it('should set to false (2)', () => {
60 | const { result } = renderHook(() => useBoolean(true))
61 |
62 | act(() => {
63 | result.current.setFalse()
64 | result.current.setFalse()
65 | })
66 |
67 | expect(result.current.value).toBe(false)
68 | })
69 |
70 | it('should toggle value', () => {
71 | const { result } = renderHook(() => useBoolean(true))
72 |
73 | act(() => {
74 | result.current.toggle()
75 | })
76 |
77 | expect(result.current.value).toBe(false)
78 | })
79 |
80 | it('should toggle value from prev using setValue', () => {
81 | const { result } = renderHook(() => useBoolean(true))
82 |
83 | act(() => {
84 | result.current.setValue(x => !x)
85 | })
86 |
87 | expect(result.current.value).toBe(false)
88 | })
89 |
90 | it('should throw an error', () => {
91 | const nonBoolean = '' as never
92 | vi.spyOn(console, 'error').mockImplementation(() => vi.fn())
93 | expect(() => {
94 | renderHook(() => useBoolean(nonBoolean))
95 | }).toThrowError(/defaultValue must be `true` or `false`/)
96 | vi.resetAllMocks()
97 | })
98 | })
99 |
--------------------------------------------------------------------------------
/apps/www/src/components/command-copy.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useState } from 'react'
4 |
5 | import { Check, Copy } from 'lucide-react'
6 | import type { ComponentProps } from 'react'
7 | import { useCopyToClipboard } from 'usehooks-ts'
8 |
9 | import { Button } from './ui/button'
10 | import {
11 | DropdownMenu,
12 | DropdownMenuContent,
13 | DropdownMenuItem,
14 | DropdownMenuTrigger,
15 | } from './ui/dropdown-menu'
16 | import { cn } from '@/lib/utils'
17 |
18 | type CommandCopyProps = {
19 | command: Record | string
20 | defaultCommand?: string
21 | } & ComponentProps<'code'>
22 |
23 | export function CommandCopy({
24 | className,
25 | command,
26 | defaultCommand,
27 | ...props
28 | }: CommandCopyProps) {
29 | const [copiedStatus, setCopiedStatus] = useState(false)
30 | const [, copy] = useCopyToClipboard()
31 |
32 | const handleCopy = (text: string) => {
33 | setCopiedStatus(true)
34 | void copy(text)
35 | setTimeout(() => {
36 | setCopiedStatus(false)
37 | }, 2000)
38 | }
39 | const renderedCommand =
40 | typeof command === 'string'
41 | ? command
42 | : command[defaultCommand ?? Object.keys(command)[0]]
43 |
44 | return (
45 |
52 |
62 |
63 |
64 |
79 |
80 |
81 | {command &&
82 | Object.entries(command).map(([key, value]) => (
83 | {
86 | handleCopy(value)
87 | }}
88 | >
89 | {key}
90 |
91 | ))}
92 |
93 |
94 |
95 | )
96 | }
97 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useMap/useMap.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'react'
2 |
3 | /**
4 | * Represents the type for either a Map or an array of key-value pairs.
5 | * @template K - The type of keys in the map.
6 | * @template V - The type of values in the map.
7 | */
8 | type MapOrEntries = Map | [K, V][]
9 |
10 | /**
11 | * Represents the actions available to interact with the map state.
12 | * @template K - The type of keys in the map.
13 | * @template V - The type of values in the map.
14 | */
15 | type UseMapActions = {
16 | /** Set a key-value pair in the map. */
17 | set: (key: K, value: V) => void
18 | /** Set all key-value pairs in the map. */
19 | setAll: (entries: MapOrEntries) => void
20 | /** Remove a key-value pair from the map. */
21 | remove: (key: K) => void
22 | /** Reset the map to an empty state. */
23 | reset: Map['clear']
24 | }
25 |
26 | /**
27 | * Represents the return type of the `useMap` hook.
28 | * We hide some setters from the returned map to disable autocompletion.
29 | * @template K - The type of keys in the map.
30 | * @template V - The type of values in the map.
31 | */
32 | type UseMapReturn = [
33 | Omit