├── packages ├── eslint-config-custom │ ├── .gitignore │ ├── index.cjs │ ├── README.md │ ├── CHANGELOG.md │ └── package.json └── usehooks-ts │ ├── .gitignore │ ├── src │ ├── useHover │ │ ├── index.ts │ │ ├── useHover.md │ │ ├── useHover.demo.tsx │ │ ├── useHover.test.ts │ │ └── useHover.ts │ ├── useMap │ │ ├── index.ts │ │ ├── useMap.md │ │ ├── useMap.demo.tsx │ │ └── useMap.ts │ ├── useStep │ │ ├── index.ts │ │ ├── useStep.md │ │ ├── useStep.demo.tsx │ │ ├── useStep.test.ts │ │ └── useStep.ts │ ├── useBoolean │ │ ├── index.ts │ │ ├── useBoolean.md │ │ ├── useBoolean.demo.tsx │ │ ├── useBoolean.ts │ │ └── useBoolean.test.ts │ ├── useCounter │ │ ├── index.ts │ │ ├── useCounter.md │ │ ├── useCounter.demo.tsx │ │ ├── useCounter.ts │ │ └── useCounter.test.ts │ ├── useScreen │ │ ├── index.ts │ │ ├── useScreen.demo.tsx │ │ └── useScreen.md │ ├── useScript │ │ ├── index.ts │ │ ├── useScript.md │ │ ├── useScript.demo.tsx │ │ └── useScript.test.ts │ ├── useTimeout │ │ ├── index.ts │ │ ├── useTimeout.md │ │ ├── useTimeout.demo.tsx │ │ ├── useTimeout.test.ts │ │ └── useTimeout.ts │ ├── useToggle │ │ ├── index.ts │ │ ├── useToggle.md │ │ ├── useToggle.demo.tsx │ │ ├── useToggle.ts │ │ └── useToggle.test.ts │ ├── useUnmount │ │ ├── index.ts │ │ ├── useUnmount.md │ │ ├── useUnmount.demo.tsx │ │ ├── useUnmount.test.ts │ │ └── useUnmount.ts │ ├── useCountdown │ │ ├── index.ts │ │ ├── useCountdown.md │ │ └── useCountdown.demo.tsx │ ├── useDarkMode │ │ ├── index.ts │ │ ├── useDarkMode.md │ │ └── useDarkMode.demo.tsx │ ├── useInterval │ │ ├── index.ts │ │ ├── useInterval.md │ │ ├── useInterval.demo.tsx │ │ ├── useInterval.ts │ │ └── useInterval.test.ts │ ├── useIsClient │ │ ├── index.ts │ │ ├── useIsClient.md │ │ ├── useIsClient.demo.tsx │ │ ├── useIsClient.test.ts │ │ └── useIsClient.ts │ ├── useIsMounted │ │ ├── index.ts │ │ ├── useIsMounted.test.ts │ │ ├── useIsMounted.ts │ │ ├── useIsMounted.demo.tsx │ │ └── useIsMounted.md │ ├── useMediaQuery │ │ ├── index.ts │ │ ├── useMediaQuery.demo.tsx │ │ ├── useMediaQuery.md │ │ └── useMediaQuery.ts │ ├── useScrollLock │ │ ├── index.ts │ │ ├── useScrollLock.md │ │ └── useScrollLock.demo.tsx │ ├── useWindowSize │ │ ├── index.ts │ │ ├── useWindowSize.demo.tsx │ │ ├── useWindowSize.md │ │ └── useWindowSize.test.ts │ ├── useLocalStorage │ │ ├── index.ts │ │ ├── useLocalStorage.demo.tsx │ │ └── useLocalStorage.md │ ├── useClickAnyWhere │ │ ├── index.ts │ │ ├── useClickAnyWhere.md │ │ ├── useClickAnyWhere.demo.tsx │ │ ├── useClickAnyWhere.ts │ │ └── useClickAnyWhere.test.ts │ ├── useCopyToClipboard │ │ ├── index.ts │ │ ├── useCopyToClipboard.md │ │ ├── useCopyToClipboard.demo.tsx │ │ ├── useCopyToClipboard.test.ts │ │ └── useCopyToClipboard.ts │ ├── useDebounceValue │ │ ├── index.ts │ │ ├── useDebounceValue.demo.tsx │ │ ├── useDebounceValue.md │ │ ├── useDebounceValue.test.ts │ │ └── useDebounceValue.ts │ ├── useDocumentTitle │ │ ├── index.ts │ │ ├── useDocumentTitle.demo.tsx │ │ ├── useDocumentTitle.md │ │ ├── useDocumentTitle.test.ts │ │ └── useDocumentTitle.ts │ ├── useEventCallback │ │ ├── index.ts │ │ ├── useEventCallback.demo.tsx │ │ ├── useEventCallback.md │ │ ├── useEventCallback.ts │ │ └── useEventCallback.test.tsx │ ├── useEventListener │ │ ├── index.ts │ │ ├── useEventListener.md │ │ └── useEventListener.demo.tsx │ ├── useOnClickOutside │ │ ├── index.ts │ │ ├── useOnClickOutside.md │ │ ├── useOnClickOutside.demo.tsx │ │ └── useOnClickOutside.ts │ ├── useResizeObserver │ │ ├── index.ts │ │ ├── useResizeObserver.md │ │ ├── useResizeObserver.demo.tsx │ │ └── useResizeObserver.test.tsx │ ├── useSessionStorage │ │ ├── index.ts │ │ ├── useSessionStorage.md │ │ └── useSessionStorage.demo.tsx │ ├── useTernaryDarkMode │ │ ├── index.ts │ │ ├── useTernaryDarkMode.md │ │ └── useTernaryDarkMode.demo.tsx │ ├── useDebounceCallback │ │ ├── index.ts │ │ ├── useDebounceCallback.demo.tsx │ │ └── useDebounceCallback.md │ ├── useReadLocalStorage │ │ ├── index.ts │ │ ├── useReadLocalStorage.demo.tsx │ │ ├── useReadLocalStorage.test.ts │ │ └── useReadLocalStorage.md │ ├── useIntersectionObserver │ │ ├── index.ts │ │ ├── useIntersectionObserver.demo.tsx │ │ └── useIntersectionObserver.md │ ├── useIsomorphicLayoutEffect │ │ ├── index.ts │ │ ├── useIsomorphicLayoutEffect.demo.tsx │ │ ├── useIsomorphicLayoutEffect.ts │ │ └── useIsomorphicLayoutEffect.md │ └── index.ts │ ├── vitest.config.ts │ ├── tests │ ├── setup.ts │ └── mocks.ts │ ├── tsup.config.ts │ ├── tsconfig.json │ └── package.json ├── pnpm-workspace.yaml ├── apps └── www │ ├── src │ ├── components │ │ ├── doc-search │ │ │ ├── index.ts │ │ │ ├── use-cmd-k.ts │ │ │ ├── types.ts │ │ │ ├── doc-search.tsx │ │ │ ├── command-menu.tsx │ │ │ ├── open-button.tsx │ │ │ ├── input.tsx │ │ │ ├── modal.context.tsx │ │ │ └── hits.tsx │ │ ├── carbon-ads │ │ │ ├── index.ts │ │ │ ├── ads.tsx │ │ │ └── use-script.ts │ │ ├── docs │ │ │ ├── right-sidebar.tsx │ │ │ ├── page-header.tsx │ │ │ ├── pager.tsx │ │ │ └── left-sidebar.tsx │ │ ├── mobile-nav.tsx │ │ ├── ui │ │ │ └── button.tsx │ │ ├── main-nav.tsx │ │ └── command-copy.tsx │ ├── assets │ │ └── fonts │ │ │ ├── CalSans-SemiBold.ttf │ │ │ ├── CalSans-SemiBold.woff │ │ │ └── CalSans-SemiBold.woff2 │ ├── config │ │ ├── marketing.ts │ │ ├── site.ts │ │ └── docs.ts │ ├── lib │ │ ├── utils.ts │ │ └── api.ts │ ├── types │ │ └── index.ts │ └── app │ │ ├── (marketing) │ │ └── layout.tsx │ │ ├── (docs) │ │ ├── layout.tsx │ │ └── introduction │ │ │ └── page.tsx │ │ └── globals.css │ ├── public │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ └── site.webmanifest │ ├── postcss.config.cjs │ ├── .eslintrc.cjs │ ├── next-sitemap.config.js │ ├── .gitignore │ ├── next.config.js │ ├── tsconfig.json │ ├── env.js │ ├── package.json │ └── tailwind.config.js ├── turbo └── generators │ ├── templates │ ├── index.ts.hbs │ └── hook │ │ ├── hook.mdx.hbs │ │ ├── index.ts.hbs │ │ ├── hook.demo.tsx.hbs │ │ ├── hook.test.ts.hbs │ │ └── hook.ts.hbs │ └── config.cts ├── .github ├── screenshot.png ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── update-docs.yml │ └── bug-report.yml ├── FUNDING.yml └── workflows │ ├── ci.yml │ └── update-algolia-index.yml ├── .prettierignore ├── .prettierrc ├── .editorconfig ├── renovate.json ├── scripts ├── env.js ├── utils │ ├── update-readme.js │ ├── get-markdown-data.js │ ├── get-hooks.js │ └── generate-doc-files.js ├── generate-doc.js ├── update-algolia-index.js └── update-testing-issue.js ├── .changeset ├── config.json └── README.md ├── .gitignore ├── turbo.json ├── LICENSE ├── typedoc.json ├── .vscode └── settings.json └── package.json /packages/eslint-config-custom/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .turbo 3 | dist 4 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useHover/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useHover' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useMap/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useMap' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useStep/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useStep' 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'apps/*' 3 | - 'packages/*' 4 | -------------------------------------------------------------------------------- /apps/www/src/components/doc-search/index.ts: -------------------------------------------------------------------------------- 1 | export * from './doc-search' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useBoolean/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useBoolean' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useCounter/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useCounter' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useScreen/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useScreen' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useScript/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useScript' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useTimeout/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useTimeout' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useToggle/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useToggle' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useUnmount/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useUnmount' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useCountdown/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useCountdown' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useDarkMode/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useDarkMode' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useInterval/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useInterval' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useIsClient/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useIsClient' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useIsMounted/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useIsMounted' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useMediaQuery/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useMediaQuery' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useScrollLock/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useScrollLock' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useWindowSize/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useWindowSize' 2 | -------------------------------------------------------------------------------- /turbo/generators/templates/index.ts.hbs: -------------------------------------------------------------------------------- 1 | export * from './{{camelCase name}}' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useLocalStorage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useLocalStorage' 2 | -------------------------------------------------------------------------------- /turbo/generators/templates/hook/hook.mdx.hbs: -------------------------------------------------------------------------------- 1 | This hook description markdown text. 2 | -------------------------------------------------------------------------------- /turbo/generators/templates/hook/index.ts.hbs: -------------------------------------------------------------------------------- 1 | export * from './{{camelCase name}}' 2 | -------------------------------------------------------------------------------- /apps/www/src/components/carbon-ads/index.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | export * from './ads' 4 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useClickAnyWhere/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useClickAnyWhere' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useCopyToClipboard/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useCopyToClipboard' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useDebounceValue/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useDebounceValue' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useDocumentTitle/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useDocumentTitle' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useEventCallback/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useEventCallback' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useEventListener/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useEventListener' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useOnClickOutside/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useOnClickOutside' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useResizeObserver/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useResizeObserver' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useSessionStorage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useSessionStorage' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useTernaryDarkMode/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useTernaryDarkMode' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useDebounceCallback/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useDebounceCallback' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useReadLocalStorage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useReadLocalStorage' 2 | -------------------------------------------------------------------------------- /.github/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juliencrn/usehooks-ts/HEAD/.github/screenshot.png -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useIntersectionObserver/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useIntersectionObserver' 2 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useIsomorphicLayoutEffect/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useIsomorphicLayoutEffect' 2 | -------------------------------------------------------------------------------- /apps/www/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juliencrn/usehooks-ts/HEAD/apps/www/public/favicon.ico -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useStep/useStep.md: -------------------------------------------------------------------------------- 1 | A simple abstraction to play with a stepper, don't repeat yourself. 2 | -------------------------------------------------------------------------------- /apps/www/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juliencrn/usehooks-ts/HEAD/apps/www/public/favicon-16x16.png -------------------------------------------------------------------------------- /apps/www/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juliencrn/usehooks-ts/HEAD/apps/www/public/favicon-32x32.png -------------------------------------------------------------------------------- /packages/eslint-config-custom/index.cjs: -------------------------------------------------------------------------------- 1 | const eslintrc = require('./.eslintrc.cjs') 2 | 3 | module.exports = eslintrc 4 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useCounter/useCounter.md: -------------------------------------------------------------------------------- 1 | A simple abstraction to play with a counter, don't repeat yourself. 2 | -------------------------------------------------------------------------------- /apps/www/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /apps/www/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juliencrn/usehooks-ts/HEAD/apps/www/public/apple-touch-icon.png -------------------------------------------------------------------------------- /apps/www/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juliencrn/usehooks-ts/HEAD/apps/www/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /apps/www/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juliencrn/usehooks-ts/HEAD/apps/www/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /apps/www/src/assets/fonts/CalSans-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juliencrn/usehooks-ts/HEAD/apps/www/src/assets/fonts/CalSans-SemiBold.ttf -------------------------------------------------------------------------------- /apps/www/src/assets/fonts/CalSans-SemiBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juliencrn/usehooks-ts/HEAD/apps/www/src/assets/fonts/CalSans-SemiBold.woff -------------------------------------------------------------------------------- /apps/www/src/assets/fonts/CalSans-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juliencrn/usehooks-ts/HEAD/apps/www/src/assets/fonts/CalSans-SemiBold.woff2 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | templates 3 | dist 4 | .turbo 5 | public 6 | .cache 7 | pnpm-lock.yaml 8 | .changeset 9 | .github 10 | apps/www/.next 11 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useIsClient/useIsClient.md: -------------------------------------------------------------------------------- 1 | This React Hook can be useful in a SSR environment to wait until be in a browser to execution some functions. 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "semi": false, 4 | "printWidth": 80, 5 | "singleQuote": true, 6 | "tabWidth": 2, 7 | "trailingComma": "all" 8 | } 9 | -------------------------------------------------------------------------------- /apps/www/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['next/core-web-vitals', 'custom'], 3 | rules: { 4 | '@typescript-eslint/require-await': 'off', 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/README.md: -------------------------------------------------------------------------------- 1 | # eslint-config-custom 2 | 3 | ## Usage 4 | 5 | in your .eslintrc 6 | 7 | ```json 8 | { 9 | "extends": ["custom-config"] 10 | } 11 | ``` 12 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useBoolean/useBoolean.md: -------------------------------------------------------------------------------- 1 | A simple abstraction to play with a boolean, don't repeat yourself. 2 | 3 | Related hooks: 4 | 5 | - [`useToggle()`](/react-hook/use-toggle) 6 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useToggle/useToggle.md: -------------------------------------------------------------------------------- 1 | A simple abstraction to play with a boolean, don't repeat yourself. 2 | 3 | Related hooks: 4 | 5 | - [`useBoolean()`](/react-hook/use-boolean) 6 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useDocumentTitle/useDocumentTitle.demo.tsx: -------------------------------------------------------------------------------- 1 | import { useDocumentTitle } from './useDocumentTitle' 2 | 3 | export default function Component() { 4 | useDocumentTitle('foo bar') 5 | } 6 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useHover/useHover.md: -------------------------------------------------------------------------------- 1 | React UI sensor hook that determine if the mouse element is in the hover element using Typescript instead CSS. 2 | This way you can separate the logic from the UI. 3 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useUnmount/useUnmount.md: -------------------------------------------------------------------------------- 1 | Hook that runs a cleanup function when the component is unmounted. 2 | 3 | ### Parameters 4 | 5 | - `func`: The cleanup function to be executed on unmount. 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useOnClickOutside/useOnClickOutside.md: -------------------------------------------------------------------------------- 1 | React hook for listening for clicks outside of a specified element (see `useRef`). 2 | 3 | This can be useful for closing a modal, a dropdown menu etc. 4 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useClickAnyWhere/useClickAnyWhere.md: -------------------------------------------------------------------------------- 1 | This simple React hook offers you a click event listener at the page level, don't repeat yourself. 2 | 3 | It is made on the [useEventListener](/react-hook/use-event-listener). 4 | -------------------------------------------------------------------------------- /turbo/generators/templates/hook/hook.demo.tsx.hbs: -------------------------------------------------------------------------------- 1 | import { {{camelCase name}} } from './{{camelCase name}}' 2 | 3 | export default function Component() { 4 | const [two] = {{camelCase name}}() 5 | 6 | return
Hello {two}
7 | } 8 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useIsClient/useIsClient.demo.tsx: -------------------------------------------------------------------------------- 1 | import { useIsClient } from './useIsClient' 2 | 3 | export default function Component() { 4 | const isClient = useIsClient() 5 | 6 | return
{isClient ? 'Client' : 'server'}
7 | } 8 | -------------------------------------------------------------------------------- /packages/usehooks-ts/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | environment: 'jsdom', 7 | setupFiles: './tests/setup.ts', 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useUnmount/useUnmount.demo.tsx: -------------------------------------------------------------------------------- 1 | import { useUnmount } from './useUnmount' 2 | 3 | export default function Component() { 4 | useUnmount(() => { 5 | // Cleanup logic here 6 | }) 7 | 8 | return
Hello world
9 | } 10 | -------------------------------------------------------------------------------- /apps/www/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useDocumentTitle/useDocumentTitle.md: -------------------------------------------------------------------------------- 1 | An easy way to set the title of the current document. 2 | 3 | Setting `preserveTitleOnUnmount` to `false` allows the document title to be reset to its default value (defined by the `` tag) when the component is unmounted. 4 | -------------------------------------------------------------------------------- /packages/usehooks-ts/tests/setup.ts: -------------------------------------------------------------------------------- 1 | import * as matchers from '@testing-library/jest-dom/matchers' 2 | import { cleanup } from '@testing-library/react' 3 | import { afterEach, expect } from 'vitest' 4 | 5 | expect.extend(matchers) 6 | 7 | afterEach(() => { 8 | cleanup() 9 | }) 10 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useScrollLock/useScrollLock.md: -------------------------------------------------------------------------------- 1 | A custom hook for locking and unlocking scroll. 2 | 3 | It can be used when you need to automatically lock the scroll, like for a modal or a sidebar. 4 | You can also use it to manually lock and unlock the scroll by disabling the `autoLock` feature. 5 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useScript/useScript.md: -------------------------------------------------------------------------------- 1 | Dynamically load an external script in one line with this React hook. This can be useful to integrate a third party library like Google Analytics or Stripe. 2 | 3 | This avoids loading this script in the `<head> </head>` on all your pages if it is not necessary. 4 | -------------------------------------------------------------------------------- /packages/usehooks-ts/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | dts: true, 6 | outDir: 'dist', 7 | clean: true, 8 | format: ['cjs', 'esm'], 9 | treeshake: true, 10 | splitting: false, 11 | cjsInterop: true, 12 | }) 13 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | "schedule:monthly", 5 | ":preserveSemverRanges", 6 | "npm:unpublishSafe", 7 | "workarounds:typesNodeVersioning", 8 | "group:allNonMajor", 9 | "helpers:disableTypesNodeMajor" 10 | ], 11 | "updateInternalDeps": true 12 | } 13 | -------------------------------------------------------------------------------- /scripts/env.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { createEnv } from '@t3-oss/env-core' 3 | import { z } from 'zod' 4 | 5 | export const env = createEnv({ 6 | server: { 7 | ALGOLIA_APP_ID: z.string(), 8 | ALGOLIA_ADMIN_KEY: z.string(), 9 | }, 10 | runtimeEnv: process.env, 11 | }) 12 | 13 | const s = env 14 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "master", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /apps/www/src/config/marketing.ts: -------------------------------------------------------------------------------- 1 | import type { MarketingConfig } from '@/types' 2 | 3 | export const marketingConfig: MarketingConfig = { 4 | mainNav: [ 5 | { 6 | title: 'Features', 7 | href: '/#features', 8 | }, 9 | { 10 | title: 'Documentation', 11 | href: '/introduction', 12 | }, 13 | ], 14 | } 15 | -------------------------------------------------------------------------------- /apps/www/src/components/doc-search/use-cmd-k.ts: -------------------------------------------------------------------------------- 1 | import { useEventListener } from 'usehooks-ts' 2 | 3 | export function useCmdK(callback: () => void) { 4 | useEventListener('keydown', event => { 5 | if (event.key === 'k' && (event.metaKey || event.ctrlKey)) { 6 | event.preventDefault() 7 | callback() 8 | } 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useScreen/useScreen.demo.tsx: -------------------------------------------------------------------------------- 1 | import { useScreen } from './useScreen' 2 | 3 | export default function Component() { 4 | const screen = useScreen() 5 | 6 | return ( 7 | <div> 8 | The current window dimensions are:{' '} 9 | <code>{JSON.stringify(screen, null, 2)}</code> 10 | </div> 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /apps/www/src/components/doc-search/types.ts: -------------------------------------------------------------------------------- 1 | type Highlight = { 2 | value: string 3 | matchLevel: string 4 | matchedWords: string[] 5 | } 6 | 7 | type Fields<T> = { 8 | objectID: T 9 | name: T 10 | summary?: T 11 | } 12 | 13 | export type Hit = Fields<string> & { 14 | __position: number 15 | _highlightResult: Fields<Highlight> 16 | } 17 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useMediaQuery/useMediaQuery.demo.tsx: -------------------------------------------------------------------------------- 1 | import { useMediaQuery } from './useMediaQuery' 2 | 3 | export default function Component() { 4 | const matches = useMediaQuery('(min-width: 768px)') 5 | 6 | return ( 7 | <div> 8 | {`The view port is ${matches ? 'at least' : 'less than'} 768 pixels wide`} 9 | </div> 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useReadLocalStorage/useReadLocalStorage.demo.tsx: -------------------------------------------------------------------------------- 1 | import { useReadLocalStorage } from './useReadLocalStorage' 2 | 3 | export default function Component() { 4 | // Assuming a value was set in localStorage with this key 5 | const darkMode = useReadLocalStorage('darkMode') 6 | 7 | return <p>DarkMode is {darkMode ? 'enabled' : 'disabled'}</p> 8 | } 9 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useEventCallback/useEventCallback.demo.tsx: -------------------------------------------------------------------------------- 1 | import { useEventCallback } from './useEventCallback' 2 | 3 | export default function Component() { 4 | const handleClick = useEventCallback(event => { 5 | // Handle the event here 6 | console.log('Clicked', event) 7 | }) 8 | 9 | return <button onClick={handleClick}>Click me</button> 10 | } 11 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useWindowSize/useWindowSize.demo.tsx: -------------------------------------------------------------------------------- 1 | import { useWindowSize } from './useWindowSize' 2 | 3 | export default function Component() { 4 | const { width = 0, height = 0 } = useWindowSize() 5 | 6 | return ( 7 | <div> 8 | The current window dimensions are:{' '} 9 | <code>{JSON.stringify({ width, height })}</code> 10 | </div> 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useClickAnyWhere/useClickAnyWhere.demo.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import { useClickAnyWhere } from './useClickAnyWhere' 4 | 5 | export default function Component() { 6 | const [count, setCount] = useState(0) 7 | 8 | useClickAnyWhere(() => { 9 | setCount(prev => prev + 1) 10 | }) 11 | 12 | return <p>Click count: {count}</p> 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 | <div ref={hoverRef}> 11 | {`The current div is ${isHover ? `hovered` : `unhovered`}`} 12 | </div> 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 <p>Hello, world</p> 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 | <div> 8 | <p>Current theme: {isDarkMode ? 'dark' : 'light'}</p> 9 | <button onClick={toggle}>Toggle</button> 10 | <button onClick={enable}>Enable</button> 11 | <button onClick={disable}>Disable</button> 12 | </div> 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 | <div> 8 | <p>Debounced value: {debouncedValue}</p> 9 | 10 | <input 11 | type="text" 12 | defaultValue={defaultValue} 13 | onChange={event => setValue(event.target.value)} 14 | /> 15 | </div> 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 | <div> 16 | <p> 17 | {visible 18 | ? "I'm visible for 5000ms" 19 | : 'You can no longer see this content'} 20 | </p> 21 | </div> 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /apps/www/next-sitemap.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate sitemap.xml and robots.txt for all kind of pages 3 | * @doc https://www.npmjs.com/package/next-sitemap 4 | */ 5 | 6 | /** @type {import('next-sitemap').IConfig} */ 7 | const config = { 8 | siteUrl: 'https://usehooks-ts.com', 9 | changefreq: 'daily', 10 | priority: 0.7, 11 | generateIndexSitemap: false, 12 | generateRobotsTxt: true, 13 | robotsTxtOptions: { 14 | policies: [ 15 | { 16 | userAgent: '*', 17 | allow: '/', 18 | }, 19 | ], 20 | }, 21 | } 22 | 23 | export default config 24 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useUnmount/useUnmount.test.ts: -------------------------------------------------------------------------------- 1 | import { act, renderHook } from '@testing-library/react' 2 | 3 | import { useUnmount } from './useUnmount' 4 | 5 | describe('useUnmount()', () => { 6 | it('should call the cleanup function on unmount', () => { 7 | const cleanupMock = vitest.fn() 8 | 9 | const { unmount } = renderHook(() => { 10 | useUnmount(cleanupMock) 11 | }) 12 | 13 | expect(cleanupMock).not.toHaveBeenCalled() 14 | 15 | act(() => { 16 | unmount() 17 | }) 18 | 19 | expect(cleanupMock).toHaveBeenCalled() 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /apps/www/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # generated 12 | /.next/ 13 | /out/ 14 | /build 15 | generated 16 | public/sitemap.xml 17 | public/robots.txt 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | 39 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useDebounceCallback/useDebounceCallback.demo.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import { useDebounceCallback } from './useDebounceCallback' 4 | 5 | export default function Component() { 6 | const [value, setValue] = useState('') 7 | 8 | const debounced = useDebounceCallback(setValue, 500) 9 | 10 | return ( 11 | <div> 12 | <p>Debounced value: {value}</p> 13 | 14 | <input 15 | type="text" 16 | defaultValue={value} 17 | onChange={event => debounced(event.target.value)} 18 | /> 19 | </div> 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /packages/usehooks-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "declaration": true, 5 | "pretty": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "jsx": "react-jsx", 9 | "strict": true, 10 | "lib": ["ESNEXT", "DOM", "DOM.Iterable"], 11 | // use global types for vite's expect, describe, etc. 12 | "types": ["vitest/globals"], 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "module": "ESNext", 16 | "moduleResolution": "Bundler" 17 | }, 18 | "exclude": ["node_modules", "dist"] 19 | } 20 | -------------------------------------------------------------------------------- /apps/www/next.config.js: -------------------------------------------------------------------------------- 1 | import './env.js' 2 | 3 | const deletedHooks = [ 4 | 'use-debounce', 5 | 'use-effect-once', 6 | 'use-element-size', 7 | 'use-fetch', 8 | 'use-image-on-load', 9 | 'use-is-first-render', 10 | 'use-locked-body', 11 | 'use-update-effect', 12 | ] 13 | 14 | /** @type {import('next').NextConfig} */ 15 | const nextConfig = { 16 | async redirects() { 17 | return deletedHooks.map(slug => ({ 18 | source: `/react-hook/${slug}`, 19 | destination: '/migrate-to-v3#removed-hooks', 20 | permanent: true, 21 | })) 22 | }, 23 | } 24 | 25 | export default nextConfig 26 | -------------------------------------------------------------------------------- /apps/www/src/config/docs.ts: -------------------------------------------------------------------------------- 1 | import type { DocsConfig } from '@/types' 2 | 3 | export const docsConfig: DocsConfig = { 4 | mainNav: [ 5 | { 6 | title: 'Documentation', 7 | href: '/introduction', 8 | }, 9 | ], 10 | sidebarNav: [ 11 | { 12 | title: 'Getting Started', 13 | items: [ 14 | { 15 | title: 'Introduction', 16 | href: '/introduction', 17 | }, 18 | { 19 | title: 'Migrate to v3', 20 | href: '/migrate-to-v3', 21 | }, 22 | ], 23 | }, 24 | // Note: Hooks are added here dynamically 25 | ], 26 | } 27 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useCounter/useCounter.demo.tsx: -------------------------------------------------------------------------------- 1 | import { useCounter } from './useCounter' 2 | 3 | export default function Component() { 4 | const { count, setCount, increment, decrement, reset } = useCounter(0) 5 | 6 | const multiplyBy2 = () => { 7 | setCount((x: number) => x * 2) 8 | } 9 | 10 | return ( 11 | <> 12 | <p>Count is {count}</p> 13 | <button onClick={increment}>Increment</button> 14 | <button onClick={decrement}>Decrement</button> 15 | <button onClick={reset}>Reset</button> 16 | <button onClick={multiplyBy2}>Multiply by 2</button> 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 | <aside className="hidden text-sm xl:block"> 12 | <div className="sticky top-16 -mt-10 max-h-[calc(var(--vh)-4rem)] overflow-y-auto pt-10 flex flex-col gap-10"> 13 | <TableOfContent toc={toc} /> 14 | 15 | <CarbonAds variant="docs" /> 16 | </div> 17 | </aside> 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<HTMLDivElement> 7 | 8 | export function PageHeader({ 9 | heading, 10 | text, 11 | className, 12 | ...props 13 | }: DocsPageHeaderProps) { 14 | return ( 15 | <> 16 | <div className={cn('space-y-4', className)} {...props}> 17 | <h1 className="inline-block font-heading text-4xl lg:text-5xl"> 18 | {heading} 19 | </h1> 20 | {text && <p className="text-xl text-muted-foreground">{text}</p>} 21 | </div> 22 | <hr className="my-4" /> 23 | </> 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useIsMounted/useIsMounted.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react' 2 | 3 | import { useIsMounted } from './useIsMounted' 4 | 5 | describe('useIsMounted()', () => { 6 | it('should return true when component is mounted', () => { 7 | const { 8 | result: { current: isMounted }, 9 | } = renderHook(() => useIsMounted()) 10 | 11 | expect(isMounted()).toBe(true) 12 | }) 13 | 14 | it('should return false when component is unmounted', () => { 15 | const { 16 | result: { current: isMounted }, 17 | unmount, 18 | } = renderHook(() => useIsMounted()) 19 | 20 | unmount() 21 | 22 | expect(isMounted()).toBe(false) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useUnmount/useUnmount.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | 3 | /** 4 | * Custom hook that runs a cleanup function when the component is unmounted. 5 | * @param {() => void} func - The cleanup function to be executed on unmount. 6 | * @public 7 | * @see [Documentation](https://usehooks-ts.com/react-hook/use-unmount) 8 | * @example 9 | * ```tsx 10 | * useUnmount(() => { 11 | * // Cleanup logic here 12 | * }); 13 | * ``` 14 | */ 15 | export function useUnmount(func: () => void) { 16 | const funcRef = useRef(func) 17 | 18 | funcRef.current = func 19 | 20 | useEffect( 21 | () => () => { 22 | funcRef.current() 23 | }, 24 | [], 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useBoolean/useBoolean.demo.tsx: -------------------------------------------------------------------------------- 1 | import { useBoolean } from './useBoolean' 2 | 3 | export default function Component() { 4 | const { value, setValue, setTrue, setFalse, toggle } = useBoolean(false) 5 | 6 | // Just an example to use "setValue" 7 | const customToggle = () => { 8 | setValue((x: boolean) => !x) 9 | } 10 | 11 | return ( 12 | <> 13 | <p> 14 | Value is <code>{value.toString()}</code> 15 | </p> 16 | <button onClick={setTrue}>set true</button> 17 | <button onClick={setFalse}>set false</button> 18 | <button onClick={toggle}>toggle</button> 19 | <button onClick={customToggle}>custom toggle</button> 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 | <button 22 | ref={ref} 23 | onClick={handleClickInside} 24 | style={{ width: 200, height: 200, background: 'cyan' }} 25 | /> 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 | <div> 8 | <p>Count: {value}</p> 9 | <button 10 | onClick={() => { 11 | setValue((x: number) => x + 1) 12 | }} 13 | > 14 | Increment 15 | </button> 16 | <button 17 | onClick={() => { 18 | setValue((x: number) => x - 1) 19 | }} 20 | > 21 | Decrement 22 | </button> 23 | <button 24 | onClick={() => { 25 | removeValue() 26 | }} 27 | > 28 | Reset 29 | </button> 30 | </div> 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 | <br /> 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 | <div> 8 | <p>Count: {value}</p> 9 | <button 10 | onClick={() => { 11 | setValue((x: number) => x + 1) 12 | }} 13 | > 14 | Increment 15 | </button> 16 | <button 17 | onClick={() => { 18 | setValue((x: number) => x - 1) 19 | }} 20 | > 21 | Decrement 22 | </button> 23 | <button 24 | onClick={() => { 25 | removeValue() 26 | }} 27 | > 28 | Reset 29 | </button> 30 | </div> 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "pipeline": { 4 | "build": { 5 | "dependsOn": ["^build"], 6 | "outputs": ["dist", ".next/**", "!.next/cache/**"], 7 | "cache": false 8 | }, 9 | "lint": { 10 | "outputs": [], 11 | "cache": false 12 | }, 13 | "test": { 14 | "outputs": [], 15 | "cache": false 16 | }, 17 | "dev": { 18 | "dependsOn": ["^build"], 19 | "outputs": [], 20 | "cache": false 21 | }, 22 | "clean": { 23 | "outputs": [], 24 | "cache": false 25 | }, 26 | "generate-doc": { 27 | "dependsOn": ["usehooks#build"], 28 | "outputs": ["generated/**", "README.md", "packages/usehooks-ts/README.md"] 29 | } 30 | }, 31 | "globalDependencies": ["tsconfig.json"] 32 | } 33 | -------------------------------------------------------------------------------- /apps/www/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./src/*"] 24 | } 25 | }, 26 | "include": [ 27 | "next-env.d.ts", 28 | "**/*.ts", 29 | "**/*.tsx", 30 | ".next/types/**/*.ts", 31 | "**/*.js" 32 | ], 33 | "exclude": ["node_modules"] 34 | } 35 | -------------------------------------------------------------------------------- /apps/www/src/components/carbon-ads/ads.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentPropsWithoutRef } from 'react' 2 | 3 | import { useScript } from './use-script' 4 | import { cn } from '@/lib/utils' 5 | 6 | const adIds = { 7 | home: 'CWYIE23E', 8 | docs: 'CWYIEKJU', 9 | } as const 10 | 11 | type CarbonAdsProps = { 12 | variant: keyof typeof adIds 13 | /** @default cover */ 14 | format?: 'responsive' | 'cover' 15 | } & ComponentPropsWithoutRef<'div'> 16 | 17 | export function CarbonAds({ 18 | variant, 19 | format = 'cover', 20 | className, 21 | ...props 22 | }: CarbonAdsProps) { 23 | const ref = useScript( 24 | `//cdn.carbonads.com/carbon.js?serve=${adIds[variant]}&placement=usehooks-tscom&format=${format}`, 25 | '_carbonads_js', 26 | ) 27 | 28 | return <div {...props} className={cn('carbon-wrap', className)} ref={ref} /> 29 | } 30 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useLayoutEffect } from 'react' 2 | 3 | /** 4 | * Custom hook that uses either `useLayoutEffect` or `useEffect` based on the environment (client-side or server-side). 5 | * @param {Function} effect - The effect function to be executed. 6 | * @param {Array<any>} [dependencies] - An array of dependencies for the effect (optional). 7 | * @public 8 | * @see [Documentation](https://usehooks-ts.com/react-hook/use-isomorphic-layout-effect) 9 | * @example 10 | * ```tsx 11 | * useIsomorphicLayoutEffect(() => { 12 | * // Code to be executed during the layout phase on the client side 13 | * }, [dependency1, dependency2]); 14 | * ``` 15 | */ 16 | export const useIsomorphicLayoutEffect = 17 | typeof window !== 'undefined' ? useLayoutEffect : useEffect 18 | -------------------------------------------------------------------------------- /scripts/utils/update-readme.js: -------------------------------------------------------------------------------- 1 | import { path, fs } from 'zx' 2 | 3 | const readmeFile = path.resolve('./README.md') 4 | const readmeUseHook = path.resolve('./packages/usehooks-ts/README.md') 5 | 6 | export function updateReadme(hooks) { 7 | const data = fs 8 | .readFileSync(readmeFile, 'utf-8') 9 | .replace( 10 | /<!-- HOOKS:START -->(.*)<!-- HOOKS:END -->/gms, 11 | `<!-- HOOKS:START -->\n\n${hooks.map(formatHook).join('\n')}\n<!-- HOOKS:END -->`, 12 | ) 13 | 14 | fs.writeFileSync(readmeFile, data, 'utf-8') 15 | fs.writeFileSync(readmeUseHook, data, 'utf-8') 16 | } 17 | 18 | // Utils 19 | 20 | function formatHook(hook) { 21 | const trimmedSummary = hook.summary 22 | .replace(/^Custom hook that /, '') 23 | .replace(/`/g, '') 24 | return `- [\`${hook.name}\`](${hook.links.doc}) — ${trimmedSummary}` 25 | } 26 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-config-custom", 3 | "private": true, 4 | "version": "2.0.0", 5 | "description": "Base configuration for Eslint", 6 | "main": "index.cjs", 7 | "author": "Julien CARON <juliencaron@protonmail.com>", 8 | "license": "MIT", 9 | "type": "module", 10 | "devDependencies": { 11 | "@typescript-eslint/eslint-plugin": "^7.0.2", 12 | "@typescript-eslint/parser": "^7.0.2", 13 | "eslint-config-prettier": "^9.1.0", 14 | "eslint-plugin-import": "^2.29.1", 15 | "eslint-plugin-jsx-a11y": "^6.8.0", 16 | "eslint-plugin-prettier": "^5.1.3", 17 | "eslint-plugin-react": "^7.33.2", 18 | "eslint-plugin-react-hooks": "^4.6.0", 19 | "eslint-plugin-simple-import-sort": "^12.0.0", 20 | "eslint-plugin-vitest": "^0.4.0", 21 | "typescript": "^5.3.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useToggle/useToggle.demo.tsx: -------------------------------------------------------------------------------- 1 | import { useToggle } from './useToggle' 2 | 3 | export default function Component() { 4 | const [value, toggle, setValue] = useToggle() 5 | 6 | // Just an example to use "setValue" 7 | const customToggle = () => { 8 | setValue((x: boolean) => !x) 9 | } 10 | 11 | return ( 12 | <> 13 | <p> 14 | Value is <code>{value.toString()}</code> 15 | </p> 16 | <button 17 | onClick={() => { 18 | setValue(true) 19 | }} 20 | > 21 | set true 22 | </button> 23 | <button 24 | onClick={() => { 25 | setValue(false) 26 | }} 27 | > 28 | set false 29 | </button> 30 | <button onClick={toggle}>toggle</button> 31 | <button onClick={customToggle}>custom toggle</button> 32 | </> 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /apps/www/src/components/doc-search/doc-search.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import algoliasearch from 'algoliasearch/lite' 4 | import { InstantSearch } from 'react-instantsearch' 5 | 6 | import { CommandMenu } from './command-menu' 7 | import { CommandMenuProvider } from './modal.context' 8 | import { OpenButton } from './open-button' 9 | 10 | const searchClient = algoliasearch( 11 | process.env.NEXT_PUBLIC_ALGOLIA_APP_ID ?? '', 12 | process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY ?? '', 13 | ) 14 | 15 | export const DocSearch = () => { 16 | return ( 17 | <CommandMenuProvider> 18 | <OpenButton /> 19 | <InstantSearch 20 | searchClient={searchClient} 21 | indexName="hooks" 22 | future={{ preserveSharedStateOnUnmount: true }} 23 | > 24 | <CommandMenu /> 25 | </InstantSearch> 26 | </CommandMenuProvider> 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useCopyToClipboard/useCopyToClipboard.demo.tsx: -------------------------------------------------------------------------------- 1 | import { useCopyToClipboard } from './useCopyToClipboard' 2 | 3 | export default function Component() { 4 | const [copiedText, copy] = useCopyToClipboard() 5 | 6 | const handleCopy = (text: string) => () => { 7 | copy(text) 8 | .then(() => { 9 | console.log('Copied!', { text }) 10 | }) 11 | .catch(error => { 12 | console.error('Failed to copy!', error) 13 | }) 14 | } 15 | 16 | return ( 17 | <> 18 | <h1>Click to copy:</h1> 19 | <div style={{ display: 'flex' }}> 20 | <button onClick={handleCopy('A')}>A</button> 21 | <button onClick={handleCopy('B')}>B</button> 22 | <button onClick={handleCopy('C')}>C</button> 23 | </div> 24 | <p>Copied value: {copiedText ?? 'Nothing is copied yet!'}</p> 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 | <p>Current step is {currentStep}</p> 18 | <p>Can go to previous step {canGoToPrevStep ? 'yes' : 'no'}</p> 19 | <p>Can go to next step {canGoToNextStep ? 'yes' : 'no'}</p> 20 | <button onClick={goToNextStep}>Go to next step</button> 21 | <button onClick={goToPrevStep}>Go to previous step</button> 22 | <button onClick={reset}>Reset</button> 23 | <button 24 | onClick={() => { 25 | setStep(3) 26 | }} 27 | > 28 | Set to step 3 29 | </button> 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 | <div 14 | ref={ref} 15 | style={{ 16 | minHeight: '100vh', 17 | display: 'flex', 18 | border: '1px dashed #000', 19 | fontSize: '2rem', 20 | }} 21 | > 22 | <div style={{ margin: 'auto' }}>{props.title}</div> 23 | </div> 24 | ) 25 | } 26 | 27 | export default function Component() { 28 | return ( 29 | <> 30 | {Array.from({ length: 5 }).map((_, index) => ( 31 | <Section key={index + 1} title={`${index + 1}`} /> 32 | ))} 33 | </> 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useIsMounted/useIsMounted.demo.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | import { useIsMounted } from './useIsMounted' 4 | 5 | const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) 6 | 7 | function Child() { 8 | const [data, setData] = useState('loading') 9 | const isMounted = useIsMounted() 10 | 11 | // simulate an api call and update state 12 | useEffect(() => { 13 | void delay(3000).then(() => { 14 | if (isMounted()) setData('OK') 15 | }) 16 | }, [isMounted]) 17 | 18 | return <p>{data}</p> 19 | } 20 | 21 | export default function Component() { 22 | const [isVisible, setVisible] = useState<boolean>(false) 23 | 24 | const toggleVisibility = () => { 25 | setVisible(state => !state) 26 | } 27 | 28 | return ( 29 | <> 30 | <button onClick={toggleVisibility}>{isVisible ? 'Hide' : 'Show'}</button> 31 | 32 | {isVisible && <Child />} 33 | </> 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useScript/useScript.demo.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | 3 | import { useScript } from './useScript' 4 | 5 | // it's an example, use your types instead 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | declare const jQuery: any 8 | 9 | export default function Component() { 10 | // Load the script asynchronously 11 | const status = useScript(`https://code.jquery.com/jquery-3.5.1.min.js`, { 12 | removeOnUnmount: false, 13 | id: 'jquery', 14 | }) 15 | 16 | useEffect(() => { 17 | if (typeof jQuery !== 'undefined') { 18 | // jQuery is loaded => print the version 19 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 20 | alert(jQuery.fn.jquery) 21 | } 22 | }, [status]) 23 | 24 | return ( 25 | <div> 26 | <p>{`Current status: ${status}`}</p> 27 | 28 | {status === 'ready' && <p>You can use the script here.</p>} 29 | </div> 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<HTMLDivElement>(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 <div>Modal</div> 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 | <div id="scrollable" style={{ maxHeight: '50vh', overflow: 'scroll' }}> 19 | {['red', 'blue', 'green'].map(color => ( 20 | <div key={color} style={{ backgroundColor: color, height: '30vh' }} /> 21 | ))} 22 | </div> 23 | 24 | <div style={{ gap: 16, display: 'flex' }}> 25 | <button onClick={lock}>Lock</button> 26 | <button onClick={unlock}>Unlock</button> 27 | </div> 28 | </> 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /apps/www/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { IconNode } from 'lucide-react' 2 | 3 | export type SiteConfig = { 4 | name: string 5 | description: string 6 | url: string 7 | ogImage: string 8 | links: { 9 | github: string 10 | npm: string 11 | } 12 | } 13 | 14 | export type BaseHook = { 15 | name: string 16 | slug: string 17 | summary: string 18 | path: string 19 | } 20 | 21 | export type NavItem = { 22 | title: string 23 | href: string 24 | disabled?: boolean 25 | } 26 | 27 | export type MainNavItem = NavItem 28 | 29 | export type SidebarNavItem = { 30 | title: string 31 | disabled?: boolean 32 | external?: boolean 33 | icon?: keyof IconNode 34 | } & ( 35 | | { 36 | href: string 37 | items?: never 38 | } 39 | | { 40 | href?: string 41 | items: NavItem[] 42 | } 43 | ) 44 | 45 | export type DocsConfig = { 46 | mainNav: MainNavItem[] 47 | sidebarNav: SidebarNavItem[] 48 | } 49 | 50 | export type MarketingConfig = { 51 | mainNav: MainNavItem[] 52 | } 53 | -------------------------------------------------------------------------------- /turbo/generators/templates/hook/hook.ts.hbs: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import type { Dispatch, SetStateAction } from 'react' 4 | 5 | /** Hook return type */ 6 | type {{pascalCase name}}ReturnType = { 7 | /** The value of ... */ 8 | value: number 9 | /** A method to update the value of ... */ 10 | setValue: Dispatch<SetStateAction<number>> 11 | } 12 | 13 | /** 14 | * Custom hook that ... 15 | * @param {boolean} [defaultValue] - The initial value for ... (default is `0`). 16 | * @returns {[number, Dispatch<SetStateAction<number>>]} A tuple containing ... 17 | * @see [Documentation](https://usehooks-ts.com/react-hook/{{kebabCase name}}) 18 | * @public 19 | * @example 20 | * ```tsx 21 | * const { value, setValue } = {{camelCase name}}(2); 22 | * 23 | * console.log(value); // 2 24 | * ``` 25 | */ 26 | export function {{camelCase name}}( 27 | defaultValue?: number, 28 | ): {{pascalCase name}}ReturnType { 29 | const [value, setValue] = useState(defaultValue || 0) 30 | 31 | return { value, setValue } 32 | } 33 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useCountdown/useCountdown.demo.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import type { ChangeEvent } from 'react' 4 | 5 | import { useCountdown } from './useCountdown' 6 | 7 | export default function Component() { 8 | const [intervalValue, setIntervalValue] = useState<number>(1000) 9 | const [count, { startCountdown, stopCountdown, resetCountdown }] = 10 | useCountdown({ 11 | countStart: 60, 12 | intervalMs: intervalValue, 13 | }) 14 | 15 | const handleChangeIntervalValue = (event: ChangeEvent<HTMLInputElement>) => { 16 | setIntervalValue(Number(event.target.value)) 17 | } 18 | return ( 19 | <div> 20 | <p>Count: {count}</p> 21 | 22 | <input 23 | type="number" 24 | value={intervalValue} 25 | onChange={handleChangeIntervalValue} 26 | /> 27 | <button onClick={startCountdown}>start</button> 28 | <button onClick={stopCountdown}>stop</button> 29 | <button onClick={resetCountdown}>reset</button> 30 | </div> 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | pull_request: 8 | 9 | env: 10 | NODE_VERSION: 20 11 | PNPM_VERSION: 8 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 15 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 2 22 | 23 | - name: Setup Pnpm 24 | uses: pnpm/action-setup@v3 25 | with: 26 | version: ${{ env.PNPM_VERSION }} 27 | 28 | - name: Setup Node.js environment 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: ${{ env.NODE_VERSION }} 32 | cache: 'pnpm' 33 | cache-dependency-path: '**/pnpm-lock.yaml' 34 | 35 | - name: Install dependencies 36 | run: pnpm install --frozen-lockfile 37 | 38 | - name: Build 39 | run: pnpm build 40 | 41 | - name: Lint 42 | run: pnpm lint 43 | 44 | - name: Test 45 | run: pnpm test 46 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useLocalStorage/useLocalStorage.md: -------------------------------------------------------------------------------- 1 | Persist the state with [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) so that it remains after a page refresh. This can be useful for a dark theme. 2 | This hook is used in the same way as useState except that you must pass the storage key in the 1st parameter. 3 | 4 | You can also pass an optional third parameter to use a custom serializer/deserializer. 5 | 6 | **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. 7 | 8 | ### Related hooks 9 | 10 | - [`useDarkMode()`](/react-hook/use-dark-mode): Helps create a dark theme switch, built on top of `useLocalStorage()`. 11 | - [`useReadLocalStorage()`](/react-hook/use-read-local-storage): Read values from local storage. 12 | - [`useSessionStorage()`](/react-hook/use-session-storage): Its implementation is almost the same of `useLocalStorage()`, but on [session storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage) instead. 13 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useTimeout/useTimeout.test.ts: -------------------------------------------------------------------------------- 1 | import { act, renderHook } from '@testing-library/react' 2 | 3 | import { useTimeout } from './useTimeout' 4 | 5 | describe('useTimeout()', () => { 6 | it('should call the callback after 1 min', () => { 7 | vitest.useFakeTimers() 8 | 9 | const delay = 60000 10 | const callback = vitest.fn() 11 | 12 | renderHook(() => { 13 | useTimeout(callback, delay) 14 | }) 15 | 16 | expect(callback).not.toHaveBeenCalled() 17 | 18 | act(() => { 19 | vitest.advanceTimersByTime(delay) 20 | }) 21 | 22 | expect(callback).toHaveBeenCalledTimes(1) 23 | }) 24 | 25 | it('should not do anything if "delay" is null', () => { 26 | vitest.useFakeTimers() 27 | 28 | const delay = null 29 | const callback = vitest.fn() 30 | 31 | renderHook(() => { 32 | useTimeout(callback, delay) 33 | }) 34 | 35 | expect(callback).not.toHaveBeenCalled() 36 | 37 | act(() => { 38 | vitest.runAllTimers() 39 | }) 40 | 41 | expect(callback).not.toHaveBeenCalled() 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /scripts/generate-doc.js: -------------------------------------------------------------------------------- 1 | import { path, fs } from 'zx' 2 | 3 | import { getHooks } from './utils/get-hooks.js' 4 | import { generateDocFiles } from './utils/generate-doc-files.js' 5 | import { updateReadme } from './utils/update-readme.js' 6 | 7 | const generatedDir = path.resolve('./generated') 8 | 9 | // Clean the generated directory 10 | await $`rimraf ${generatedDir}/docs` 11 | await $`rimraf ${generatedDir}/typedoc` 12 | 13 | // Generate base from JSDoc comments using typedoc 14 | await $`typedoc` 15 | 16 | // Read hook list from the `generated/typedoc/all.json` file 17 | const hooks = getHooks() 18 | 19 | // Create the markdown files 20 | fs.mkdirSync(path.join(generatedDir, 'docs')) 21 | fs.mkdirSync(path.join(generatedDir, 'docs', 'hooks')) 22 | 23 | for (const hook of hooks) { 24 | generateDocFiles(hook) 25 | } 26 | 27 | // Create the JSON file 28 | fs.writeFileSync( 29 | path.join(generatedDir, 'docs', 'hooks.json'), 30 | JSON.stringify(hooks, null, 2), 31 | ) 32 | 33 | // Update the README file 34 | updateReadme(hooks) 35 | 36 | // Format with Prettier 37 | await $`pnpm format --log-level error` 38 | -------------------------------------------------------------------------------- /apps/www/src/components/doc-search/command-menu.tsx: -------------------------------------------------------------------------------- 1 | import { Index } from 'react-instantsearch' 2 | 3 | import { Footer } from './footer' 4 | import { RenderHits } from './hits' 5 | import { SearchInput } from './input' 6 | import { useCommandMenuContext } from './modal.context' 7 | import { 8 | CommandDialog, 9 | CommandEmpty, 10 | CommandGroup, 11 | CommandList, 12 | } from '@/components/ui/command' 13 | 14 | export function CommandMenu() { 15 | const { open, setOpen } = useCommandMenuContext() 16 | 17 | return ( 18 | <CommandDialog open={open} onOpenChange={setOpen}> 19 | <SearchInput /> 20 | <CommandList> 21 | <Index indexName="hooks"> 22 | <CommandGroup heading="Hooks"> 23 | <RenderHits /> 24 | </CommandGroup> 25 | </Index> 26 | <Index indexName="removed-hooks"> 27 | <CommandGroup heading="Removed hooks"> 28 | <RenderHits /> 29 | </CommandGroup> 30 | </Index> 31 | <CommandEmpty>No results found.</CommandEmpty> 32 | </CommandList> 33 | <Footer /> 34 | </CommandDialog> 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useMap/useMap.demo.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react' 2 | 3 | import { useMap } from './useMap' 4 | 5 | export default function Component() { 6 | const [map, actions] = useMap<string, string>([['key', '🆕']]) 7 | 8 | const set = () => { 9 | actions.set(String(Date.now()), '📦') 10 | } 11 | const setAll = () => { 12 | actions.setAll([ 13 | ['hello', '👋'], 14 | ['data', '📦'], 15 | ]) 16 | } 17 | const reset = () => { 18 | actions.reset() 19 | } 20 | const remove = () => { 21 | actions.remove('hello') 22 | } 23 | 24 | return ( 25 | <div> 26 | <button onClick={set}>Add</button> 27 | <button onClick={reset}>Reset</button> 28 | <button onClick={setAll}>Set new data</button> 29 | <button onClick={remove} disabled={!map.get('hello')}> 30 | {'Remove "hello"'} 31 | </button> 32 | 33 | <pre> 34 | Map ( 35 | {Array.from(map.entries()).map(([key, value]) => ( 36 | <Fragment key={key}>{`\n ${key}: ${value}`}</Fragment> 37 | ))} 38 | <br />) 39 | </pre> 40 | </div> 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<HTMLButtonElement>(null) 8 | const documentRef = useRef<Document>(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 | <div style={{ minHeight: '200vh' }}> 36 | <button ref={buttonRef}>Click me</button> 37 | </div> 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<SetStateAction<boolean>>]} 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<SetStateAction<boolean>>] { 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<number>(0) 10 | // Dynamic delay 11 | const [delay, setDelay] = useState<number>(1000) 12 | // ON/OFF 13 | const [isPlaying, setPlaying] = useState<boolean>(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<HTMLInputElement>) => { 25 | setDelay(Number(event.target.value)) 26 | } 27 | 28 | return ( 29 | <> 30 | <h1>{count}</h1> 31 | <button 32 | onClick={() => { 33 | setPlaying(!isPlaying) 34 | }} 35 | > 36 | {isPlaying ? 'pause' : 'play'} 37 | </button> 38 | <p> 39 | <label htmlFor="delay">Delay: </label> 40 | <input 41 | type="number" 42 | name="delay" 43 | onChange={handleChange} 44 | value={delay} 45 | /> 46 | </p> 47 | </> 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /apps/www/src/app/(marketing)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | import { DocSearch } from '@/components/doc-search' 4 | import { MainNav } from '@/components/main-nav' 5 | import { GitHub } from '@/components/ui/icons' 6 | import { marketingConfig } from '@/config/marketing' 7 | import { siteConfig } from '@/config/site' 8 | 9 | type MarketingLayoutProps = { 10 | children: React.ReactNode 11 | } 12 | 13 | export default async function MarketingLayout({ 14 | children, 15 | }: MarketingLayoutProps) { 16 | return ( 17 | <> 18 | <header className="container z-40 bg-background"> 19 | <div className="flex h-20 items-center justify-between py-6"> 20 | <MainNav items={marketingConfig.mainNav} /> 21 | <nav className="flex space-x-4 justify-center align-middle"> 22 | <DocSearch /> 23 | 24 | <Link 25 | href={siteConfig.links.github} 26 | target="_blank" 27 | rel="noreferrer" 28 | className="flex" 29 | > 30 | <GitHub className="h-6 w-6 my-auto" /> 31 | <span className="sr-only">GitHub</span> 32 | </Link> 33 | </nav> 34 | </div> 35 | </header> 36 | 37 | <main className="flex-1">{children}</main> 38 | </> 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /apps/www/src/components/doc-search/open-button.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react' 2 | 3 | import { Search } from 'lucide-react' 4 | import type { ButtonHTMLAttributes } from 'react' 5 | 6 | import { buttonVariants } from '../ui/button' 7 | import { useCommandMenuContext } from './modal.context' 8 | import { cn } from '@/lib/utils' 9 | type ButtonProps = Omit< 10 | ButtonHTMLAttributes<HTMLButtonElement>, 11 | 'children' | 'onClick' | 'ref' 12 | > 13 | 14 | export const OpenButton = forwardRef<HTMLButtonElement, ButtonProps>( 15 | function OpenButton(props, ref) { 16 | const { handleOpen } = useCommandMenuContext() 17 | 18 | return ( 19 | <button 20 | ref={ref} 21 | onClick={handleOpen} 22 | {...props} 23 | className={cn( 24 | buttonVariants({ variant: 'secondary' }), 25 | 'hidden sm:flex items-center w-60 text-left space-x-2 text-muted-foreground', 26 | props.className, 27 | )} 28 | > 29 | <Search className="flex-none w-5 h-5" /> 30 | 31 | <span className="flex-auto">Quick search...</span> 32 | <kbd className="font-sans"> 33 | <abbr title="Command" className="no-underline"> 34 | ⌘ 35 | </abbr>{' '} 36 | K 37 | </kbd> 38 | </button> 39 | ) 40 | }, 41 | ) 42 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useTernaryDarkMode/useTernaryDarkMode.demo.tsx: -------------------------------------------------------------------------------- 1 | import { useTernaryDarkMode } from './useTernaryDarkMode' 2 | 3 | type TernaryDarkMode = ReturnType<typeof useTernaryDarkMode>['ternaryDarkMode'] 4 | 5 | export default function Component() { 6 | const { 7 | isDarkMode, 8 | ternaryDarkMode, 9 | setTernaryDarkMode, 10 | toggleTernaryDarkMode, 11 | } = useTernaryDarkMode() 12 | 13 | return ( 14 | <div> 15 | <p>Current theme: {isDarkMode ? 'dark' : 'light'}</p> 16 | <p>ternaryMode: {ternaryDarkMode}</p> 17 | <p> 18 | Toggle between three modes 19 | <button onClick={toggleTernaryDarkMode}> 20 | Toggle from {ternaryDarkMode} 21 | </button> 22 | </p> 23 | <p> 24 | Select a mode 25 | <br /> 26 | <select 27 | name="select-ternaryDarkMode" 28 | onChange={ev => { 29 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 30 | setTernaryDarkMode(ev.target.value as TernaryDarkMode) 31 | }} 32 | value={ternaryDarkMode} 33 | > 34 | <option value="light">light</option> 35 | <option value="system">system</option> 36 | <option value="dark">dark</option> 37 | </select> 38 | </p> 39 | </div> 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useHover/useHover.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import type { RefObject } from 'react' 4 | 5 | import { useEventListener } from '../useEventListener' 6 | 7 | /** 8 | * Custom hook that tracks whether a DOM element is being hovered over. 9 | * @template T - The type of the DOM element. Defaults to `HTMLElement`. 10 | * @param {RefObject<T>} elementRef - The ref object for the DOM element to track. 11 | * @returns {boolean} A boolean value indicating whether the element is being hovered over. 12 | * @public 13 | * @see [Documentation](https://usehooks-ts.com/react-hook/use-hover) 14 | * @example 15 | * ```tsx 16 | * const buttonRef = useRef<HTMLButtonElement>(null); 17 | * const isHovered = useHover(buttonRef); 18 | * // Access the isHovered variable to determine if the button is being hovered over. 19 | * ``` 20 | */ 21 | export function useHover<T extends HTMLElement = HTMLElement>( 22 | elementRef: RefObject<T>, 23 | ): boolean { 24 | const [value, setValue] = useState<boolean>(false) 25 | 26 | const handleMouseEnter = () => { 27 | setValue(true) 28 | } 29 | const handleMouseLeave = () => { 30 | setValue(false) 31 | } 32 | 33 | useEventListener('mouseenter', handleMouseEnter, elementRef) 34 | useEventListener('mouseleave', handleMouseLeave, elementRef) 35 | 36 | return value 37 | } 38 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useResizeObserver/useResizeObserver.demo.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from 'react' 2 | 3 | import { useDebounceCallback } from '../useDebounceCallback' 4 | import { useResizeObserver } from './useResizeObserver' 5 | 6 | type Size = { 7 | width?: number 8 | height?: number 9 | } 10 | 11 | export default function Component() { 12 | const ref = useRef<HTMLDivElement>(null) 13 | const { width = 0, height = 0 } = useResizeObserver({ 14 | ref, 15 | box: 'border-box', 16 | }) 17 | 18 | return ( 19 | <div ref={ref} style={{ border: '1px solid palevioletred', width: '100%' }}> 20 | {width} x {height} 21 | </div> 22 | ) 23 | } 24 | 25 | export function WithDebounce() { 26 | const ref = useRef<HTMLDivElement>(null) 27 | const [{ width, height }, setSize] = useState<Size>({ 28 | width: undefined, 29 | height: undefined, 30 | }) 31 | 32 | const onResize = useDebounceCallback(setSize, 200) 33 | 34 | useResizeObserver({ 35 | ref, 36 | onResize, 37 | }) 38 | 39 | return ( 40 | <div 41 | ref={ref} 42 | style={{ 43 | border: '1px solid palevioletred', 44 | width: '100%', 45 | resize: 'both', 46 | overflow: 'auto', 47 | maxWidth: '100%', 48 | }} 49 | > 50 | debounced: {width} x {height} 51 | </div> 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useClickAnyWhere/useClickAnyWhere.test.ts: -------------------------------------------------------------------------------- 1 | import { act, fireEvent, renderHook } from '@testing-library/react' 2 | 3 | import { useClickAnyWhere } from './useClickAnyWhere' 4 | 5 | describe('useClickAnyWhere()', () => { 6 | it('should call handler (0)', () => { 7 | const mockHandler: (event: MouseEvent) => void = vitest.fn() 8 | renderHook(() => { 9 | useClickAnyWhere(mockHandler) 10 | }) 11 | 12 | act(() => { 13 | fireEvent.doubleClick(window) 14 | }) 15 | 16 | expect(mockHandler).toHaveBeenCalledTimes(0) 17 | }) 18 | 19 | it('should call handler (1) with MouseEvent', () => { 20 | const mockHandler: (event: MouseEvent) => void = vitest.fn() 21 | 22 | renderHook(() => { 23 | useClickAnyWhere(mockHandler) 24 | }) 25 | 26 | act(() => { 27 | fireEvent.click(window) 28 | }) 29 | 30 | expect(mockHandler).toHaveBeenCalledTimes(1) 31 | expect(mockHandler).toHaveBeenCalledWith(expect.any(MouseEvent)) 32 | }) 33 | 34 | it('should call handler (2)', () => { 35 | const mockHandler: (event: MouseEvent) => void = vitest.fn() 36 | renderHook(() => { 37 | useClickAnyWhere(mockHandler) 38 | }) 39 | 40 | act(() => { 41 | fireEvent.click(window) 42 | fireEvent.click(window) 43 | }) 44 | 45 | expect(mockHandler).toHaveBeenCalledTimes(2) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useCopyToClipboard/useCopyToClipboard.test.ts: -------------------------------------------------------------------------------- 1 | import { act, renderHook } from '@testing-library/react' 2 | 3 | import { useCopyToClipboard } from './useCopyToClipboard' 4 | 5 | describe('useCopyToClipboard()', () => { 6 | const originalClipboard = { ...global.navigator.clipboard } 7 | const mockData = 'Test value' 8 | 9 | beforeEach(() => { 10 | const mockClipboard = { 11 | writeText: vitest.fn(), 12 | } 13 | // @ts-ignore mock clipboard 14 | global.navigator.clipboard = mockClipboard 15 | }) 16 | 17 | afterEach(() => { 18 | vitest.resetAllMocks() 19 | // @ts-ignore mock clipboard 20 | global.navigator.clipboard = originalClipboard 21 | }) 22 | 23 | it('should use clipboard', () => { 24 | const { result } = renderHook(() => useCopyToClipboard()) 25 | 26 | expect(result.current[0]).toBe(null) 27 | expect(typeof result.current[1]).toBe('function') 28 | }) 29 | 30 | it('should copy to the clipboard and the state', async () => { 31 | const { result } = renderHook(() => useCopyToClipboard()) 32 | 33 | await act(async () => { 34 | await result.current[1](mockData) 35 | }) 36 | 37 | expect(navigator.clipboard.writeText).toHaveBeenCalledTimes(1) 38 | expect(navigator.clipboard.writeText).toHaveBeenCalledWith(mockData) 39 | expect(result.current[0]).toBe(mockData) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useDocumentTitle/useDocumentTitle.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react' 2 | 3 | import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect' 4 | import { useUnmount } from '../useUnmount' 5 | 6 | /** Hook options. */ 7 | type UseDocumentTitleOptions = { 8 | /** Whether to keep the title after unmounting the component (default is `true`). */ 9 | preserveTitleOnUnmount?: boolean 10 | } 11 | 12 | /** 13 | * Custom hook that sets the document title. 14 | * @param {string} title - The title to set. 15 | * @param {?UseDocumentTitleOptions} [options] - The options. 16 | * @public 17 | * @see [Documentation](https://usehooks-ts.com/react-hook/use-document-title) 18 | * @example 19 | * ```tsx 20 | * useDocumentTitle('My new title'); 21 | * ``` 22 | */ 23 | export function useDocumentTitle( 24 | title: string, 25 | options: UseDocumentTitleOptions = {}, 26 | ): void { 27 | const { preserveTitleOnUnmount = true } = options 28 | const defaultTitle = useRef<string | null>(null) 29 | 30 | useIsomorphicLayoutEffect(() => { 31 | defaultTitle.current = window.document.title 32 | }, []) 33 | 34 | useIsomorphicLayoutEffect(() => { 35 | window.document.title = title 36 | }, [title]) 37 | 38 | useUnmount(() => { 39 | if (!preserveTitleOnUnmount && defaultTitle.current) { 40 | window.document.title = defaultTitle.current 41 | } 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /apps/www/src/components/doc-search/input.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from 'react' 2 | 3 | import { useSearchBox } from 'react-instantsearch' 4 | 5 | import { CommandInput } from '../ui/command' 6 | 7 | export function SearchInput() { 8 | const { query, refine } = useSearchBox() 9 | const [inputValue, setInputValue] = useState(query) 10 | const inputRef = useRef<HTMLInputElement>(null) 11 | 12 | function setQuery(newQuery: string) { 13 | setInputValue(newQuery) 14 | refine(newQuery) 15 | } 16 | 17 | return ( 18 | <form 19 | action="" 20 | role="search" 21 | noValidate 22 | onSubmit={event => { 23 | event.preventDefault() 24 | event.stopPropagation() 25 | 26 | if (inputRef.current) { 27 | inputRef.current.blur() 28 | } 29 | }} 30 | onReset={event => { 31 | event.preventDefault() 32 | event.stopPropagation() 33 | 34 | setQuery('') 35 | 36 | if (inputRef.current) { 37 | inputRef.current.focus() 38 | } 39 | }} 40 | > 41 | <CommandInput 42 | ref={inputRef} 43 | autoComplete="off" 44 | autoCorrect="off" 45 | autoCapitalize="off" 46 | placeholder="Search for hooks…" 47 | spellCheck={false} 48 | maxLength={512} 49 | value={inputValue} 50 | onValueChange={setQuery} 51 | /> 52 | </form> 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /scripts/utils/get-markdown-data.js: -------------------------------------------------------------------------------- 1 | import { path, fs } from 'zx' 2 | 3 | const typedocDir = path.resolve('./generated/typedoc') 4 | const hooksSrcDir = path.resolve('./packages/usehooks-ts/src') 5 | 6 | export function getHookDocData(hook) { 7 | const filename = `${hook.name}_${hook.name}.${hook.name}.md` 8 | const pathname = path.join(typedocDir, 'functions', filename) 9 | return getFile(pathname, 'documentation') 10 | } 11 | 12 | export function getTypeAliasesData(hook) { 13 | return ( 14 | hook.types.map(t => { 15 | const filename = `${hook.name}_${hook.name}.${t.name}.md` 16 | const pathname = path.join(typedocDir, 'types', filename) 17 | const [file] = getFile(pathname, 'type aliases') 18 | return file 19 | }) || [] 20 | ) 21 | } 22 | 23 | export function getCodeData(hook) { 24 | const pathname = path.join(hooksSrcDir, `${hook.name}`, `${hook.name}.ts`) 25 | return getFile(pathname, 'code') 26 | } 27 | 28 | export function getDemoData(hook) { 29 | const filename = `${hook.name}.demo.tsx` 30 | const pathname = path.join(hooksSrcDir, `${hook.name}`, filename) 31 | return getFile(pathname, 'demo') 32 | } 33 | 34 | // Utils 35 | 36 | function getFile(filename, type) { 37 | const file = fs.readFileSync(filename, 'utf-8') 38 | 39 | if (!file && ['code', 'demo', 'docs'].includes(type)) { 40 | const name = filename.split('/').slice(-1)[0] 41 | throw new Error(`No ${type} found for ${name}`) 42 | } 43 | 44 | return [file] 45 | } 46 | -------------------------------------------------------------------------------- /packages/usehooks-ts/tests/mocks.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Mocks the matchMedia API 3 | * @param {boolean} matches - True for dark, false for light 4 | * @example 5 | * mockMatchMedia(false) 6 | */ 7 | export const mockMatchMedia = (matches: boolean): void => { 8 | Object.defineProperty(window, 'matchMedia', { 9 | writable: true, 10 | value: vitest.fn().mockImplementation(query => ({ 11 | matches, 12 | media: query, 13 | onchange: null, 14 | addEventListener: vitest.fn(), 15 | removeEventListener: vitest.fn(), 16 | dispatchEvent: vitest.fn(), 17 | })), 18 | }) 19 | } 20 | 21 | /** 22 | * Mocks the Storage API 23 | * @param {'localStorage' | 'sessionStorage'} name - The name of the storage to mock 24 | * @example 25 | * mockStorage('localStorage') 26 | * // Then use window.localStorage as usual (it will be mocked) 27 | */ 28 | export const mockStorage = (name: 'localStorage' | 'sessionStorage'): void => { 29 | class StorageMock implements Omit<Storage, 'key' | 'length'> { 30 | store: Record<string, string> = {} 31 | 32 | clear() { 33 | this.store = {} 34 | } 35 | 36 | getItem(key: string) { 37 | return this.store[key] || null 38 | } 39 | 40 | setItem(key: string, value: unknown) { 41 | this.store[key] = value + '' 42 | } 43 | 44 | removeItem(key: string) { 45 | delete this.store[key] 46 | } 47 | } 48 | 49 | Object.defineProperty(window, name, { 50 | value: new StorageMock(), 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useEventCallback/useEventCallback.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef } from 'react' 2 | 3 | import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect' 4 | 5 | /** 6 | * Custom hook that creates a memoized event callback. 7 | * @template Args - An array of argument types for the event callback. 8 | * @template R - The return type of the event callback. 9 | * @param {(...args: Args) => R} fn - The callback function. 10 | * @returns {(...args: Args) => R} A memoized event callback function. 11 | * @public 12 | * @see [Documentation](https://usehooks-ts.com/react-hook/use-event-callback) 13 | * @example 14 | * ```tsx 15 | * const handleClick = useEventCallback((event) => { 16 | * // Handle the event here 17 | * }); 18 | * ``` 19 | */ 20 | export function useEventCallback<Args extends unknown[], R>( 21 | fn: (...args: Args) => R, 22 | ): (...args: Args) => R 23 | export function useEventCallback<Args extends unknown[], R>( 24 | fn: ((...args: Args) => R) | undefined, 25 | ): ((...args: Args) => R) | undefined 26 | export function useEventCallback<Args extends unknown[], R>( 27 | fn: ((...args: Args) => R) | undefined, 28 | ): ((...args: Args) => R) | undefined { 29 | const ref = useRef<typeof fn>(() => { 30 | throw new Error('Cannot call an event handler while rendering.') 31 | }) 32 | 33 | useIsomorphicLayoutEffect(() => { 34 | ref.current = fn 35 | }, [fn]) 36 | 37 | return useCallback((...args: Args) => ref.current?.(...args), [ref]) as ( 38 | ...args: Args 39 | ) => R 40 | } 41 | -------------------------------------------------------------------------------- /apps/www/src/components/doc-search/modal.context.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useState } from 'react' 2 | 3 | import type { Dispatch, SetStateAction } from 'react' 4 | 5 | import { useCmdK } from './use-cmd-k' 6 | 7 | type CommandMenuContextType = { 8 | open: boolean 9 | setOpen: Dispatch<SetStateAction<boolean>> 10 | handleOpen: () => void 11 | handleClose: () => void 12 | } 13 | 14 | const initialContext: CommandMenuContextType = { 15 | open: false, 16 | setOpen: () => undefined, 17 | handleOpen: () => undefined, 18 | handleClose: () => undefined, 19 | } 20 | 21 | const CommandMenuContext = createContext<CommandMenuContextType>(initialContext) 22 | 23 | export function CommandMenuProvider(props: { children: React.ReactNode }) { 24 | const [open, setOpen] = useState(initialContext.open) 25 | 26 | // Toggle the menu when ⌘K is pressed 27 | useCmdK(() => { 28 | setOpen(open => !open) 29 | }) 30 | 31 | const handleOpen = () => { 32 | setOpen(true) 33 | } 34 | 35 | const handleClose = () => { 36 | setOpen(false) 37 | } 38 | 39 | return ( 40 | <CommandMenuContext.Provider 41 | value={{ open, setOpen, handleOpen, handleClose }} 42 | > 43 | {props.children} 44 | </CommandMenuContext.Provider> 45 | ) 46 | } 47 | 48 | export function useCommandMenuContext() { 49 | const context = useContext(CommandMenuContext) 50 | 51 | if (!context) { 52 | throw new Error( 53 | '`useCommandMenuContext` must be used within a `CommandMenuProvider`', 54 | ) 55 | } 56 | 57 | return context 58 | } 59 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useInterval/useInterval.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | 3 | import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect' 4 | 5 | /** 6 | * Custom hook that creates an interval that invokes a callback function at a specified delay using the [`setInterval API`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval). 7 | * @param {() => void} callback - The function to be invoked at each interval. 8 | * @param {number | null} delay - The time, in milliseconds, between each invocation of the callback. Use `null` to clear the interval. 9 | * @public 10 | * @see [Documentation](https://usehooks-ts.com/react-hook/use-interval) 11 | * @example 12 | * ```tsx 13 | * const handleInterval = () => { 14 | * // Code to be executed at each interval 15 | * }; 16 | * useInterval(handleInterval, 1000); 17 | * ``` 18 | */ 19 | export function useInterval(callback: () => void, delay: number | null) { 20 | const savedCallback = useRef(callback) 21 | 22 | // Remember the latest callback if it changes. 23 | useIsomorphicLayoutEffect(() => { 24 | savedCallback.current = callback 25 | }, [callback]) 26 | 27 | // Set up the interval. 28 | useEffect(() => { 29 | // Don't schedule if no delay is specified. 30 | // Note: 0 is a valid value for delay. 31 | if (delay === null) { 32 | return 33 | } 34 | 35 | const id = setInterval(() => { 36 | savedCallback.current() 37 | }, delay) 38 | 39 | return () => { 40 | clearInterval(id) 41 | } 42 | }, [delay]) 43 | } 44 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useToggle/useToggle.test.ts: -------------------------------------------------------------------------------- 1 | import { act, renderHook } from '@testing-library/react' 2 | 3 | import { useToggle } from './useToggle' 4 | 5 | describe('use toggle()', () => { 6 | it('should use toggle be ok', () => { 7 | const { result } = renderHook(() => useToggle()) 8 | const [value, toggle, setValue] = result.current 9 | 10 | expect(value).toBe(false) 11 | expect(typeof toggle).toBe('function') 12 | expect(typeof setValue).toBe('function') 13 | }) 14 | 15 | it('should default value works', () => { 16 | const { result } = renderHook(() => useToggle(true)) 17 | const [value] = result.current 18 | 19 | expect(value).toBe(true) 20 | }) 21 | 22 | it('setValue should mutate the value', () => { 23 | const { result } = renderHook(() => useToggle()) 24 | const [, , setValue] = result.current 25 | 26 | expect(result.current[0]).toBe(false) 27 | 28 | act(() => { 29 | setValue(true) 30 | }) 31 | 32 | expect(result.current[0]).toBe(true) 33 | 34 | act(() => { 35 | setValue(prev => !prev) 36 | }) 37 | 38 | expect(result.current[0]).toBe(false) 39 | }) 40 | 41 | it('toggle should mutate the value', () => { 42 | const { result } = renderHook(() => useToggle()) 43 | const [, toggle] = result.current 44 | 45 | expect(result.current[0]).toBe(false) 46 | 47 | act(() => { 48 | toggle() 49 | }) 50 | 51 | expect(result.current[0]).toBe(true) 52 | 53 | act(() => { 54 | toggle() 55 | }) 56 | 57 | expect(result.current[0]).toBe(false) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /apps/www/src/components/docs/pager.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | import { buttonVariants } from '@/components/ui/button' 4 | import { ChevronLeft, ChevronRight } from '@/components/ui/icons' 5 | import { cn, mapHookToNavLink } from '@/lib/utils' 6 | import type { BaseHook } from '@/types' 7 | 8 | type DocsPagerProps = { 9 | slug: string 10 | hooks: BaseHook[] 11 | } 12 | 13 | export function Pager({ slug, hooks }: DocsPagerProps) { 14 | const { prev, next } = getPaperElements({ slug, hooks }) 15 | 16 | if (!prev && !next) { 17 | return null 18 | } 19 | 20 | return ( 21 | <div className="flex flex-row items-center justify-between"> 22 | {prev && ( 23 | <Link 24 | href={prev.href} 25 | className={cn(buttonVariants({ variant: 'ghost' }))} 26 | > 27 | <ChevronLeft className="mr-2 h-4 w-4" /> 28 | {prev.title} 29 | </Link> 30 | )} 31 | {next && ( 32 | <Link 33 | href={next.href} 34 | className={cn(buttonVariants({ variant: 'ghost' }), 'ml-auto')} 35 | > 36 | {next.title} 37 | <ChevronRight className="ml-2 h-4 w-4" /> 38 | </Link> 39 | )} 40 | </div> 41 | ) 42 | } 43 | 44 | function getPaperElements({ slug, hooks }: DocsPagerProps) { 45 | const activeIndex = hooks.findIndex(h => h.slug === slug) 46 | const links = hooks.map(mapHookToNavLink) 47 | const prev = activeIndex !== 0 ? links[activeIndex - 1] : null 48 | const next = activeIndex !== hooks.length - 1 ? links[activeIndex + 1] : null 49 | 50 | return { prev, next } 51 | } 52 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useTimeout/useTimeout.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | 3 | import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect' 4 | 5 | /** 6 | * Custom hook that handles timeouts in React components using the [`setTimeout API`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout). 7 | * @param {() => void} callback - The function to be executed when the timeout elapses. 8 | * @param {number | null} delay - The duration (in milliseconds) for the timeout. Set to `null` to clear the timeout. 9 | * @returns {void} This hook does not return anything. 10 | * @public 11 | * @see [Documentation](https://usehooks-ts.com/react-hook/use-timeout) 12 | * @example 13 | * ```tsx 14 | * // Usage of useTimeout hook 15 | * useTimeout(() => { 16 | * // Code to be executed after the specified delay 17 | * }, 1000); // Set a timeout of 1000 milliseconds (1 second) 18 | * ``` 19 | */ 20 | export function useTimeout(callback: () => void, delay: number | null): void { 21 | const savedCallback = useRef(callback) 22 | 23 | // Remember the latest callback if it changes. 24 | useIsomorphicLayoutEffect(() => { 25 | savedCallback.current = callback 26 | }, [callback]) 27 | 28 | // Set up the timeout. 29 | useEffect(() => { 30 | // Don't schedule if no delay is specified. 31 | // Note: 0 is a valid value for delay. 32 | if (!delay && delay !== 0) { 33 | return 34 | } 35 | 36 | const id = setTimeout(() => { 37 | savedCallback.current() 38 | }, delay) 39 | 40 | return () => { 41 | clearTimeout(id) 42 | } 43 | }, [delay]) 44 | } 45 | -------------------------------------------------------------------------------- /apps/www/src/components/mobile-nav.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import Link from 'next/link' 4 | import { useScrollLock } from 'usehooks-ts' 5 | 6 | import { Logo } from '@/components/ui/icons' 7 | import { siteConfig } from '@/config/site' 8 | import { cn } from '@/lib/utils' 9 | import type { MainNavItem } from '@/types' 10 | 11 | type MobileNavProps = { 12 | items: MainNavItem[] 13 | children?: React.ReactNode 14 | } 15 | 16 | export function MobileNav({ items, children }: MobileNavProps) { 17 | useScrollLock() 18 | 19 | return ( 20 | <div 21 | className={cn( 22 | 'fixed inset-0 top-16 z-50 grid h-[calc(100vh-4rem)] grid-flow-row auto-rows-max overflow-auto p-6 pb-32 shadow-md animate-in slide-in-from-bottom-80 md:hidden', 23 | )} 24 | > 25 | <div className="relative z-20 grid gap-6 rounded-md bg-popover p-4 text-popover-foreground shadow-md"> 26 | <Link href="/" className="flex items-center space-x-2"> 27 | <Logo /> 28 | <span className="font-bold">{siteConfig.name}</span> 29 | </Link> 30 | <nav className="grid grid-flow-row auto-rows-max text-sm"> 31 | {items.map((item, index) => ( 32 | <Link 33 | key={index} 34 | href={item.disabled ? '#' : item.href} 35 | className={cn( 36 | 'flex w-full items-center rounded-md p-2 text-sm font-medium hover:underline', 37 | item.disabled && 'cursor-not-allowed opacity-60', 38 | )} 39 | > 40 | {item.title} 41 | </Link> 42 | ))} 43 | </nav> 44 | {children} 45 | </div> 46 | </div> 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useCounter/useCounter.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react' 2 | 3 | import type { Dispatch, SetStateAction } from 'react' 4 | 5 | /** The hook return type. */ 6 | type UseCounterReturn = { 7 | /** The current count value. */ 8 | count: number 9 | /** Function to increment the counter by 1. */ 10 | increment: () => void 11 | /** Function to decrement the counter by 1. */ 12 | decrement: () => void 13 | /** Function to reset the counter to its initial value. */ 14 | reset: () => void 15 | /** Function to set a specific value to the counter. */ 16 | setCount: Dispatch<SetStateAction<number>> 17 | } 18 | 19 | /** 20 | * Custom hook that manages a counter with increment, decrement, reset, and setCount functionalities. 21 | * @param {number} [initialValue] - The initial value for the counter. 22 | * @returns {UseCounterReturn} An object containing the current count and functions to interact with the counter. 23 | * @public 24 | * @see [Documentation](https://usehooks-ts.com/react-hook/use-counter) 25 | * @example 26 | * ```tsx 27 | * const { count, increment, decrement, reset, setCount } = useCounter(5); 28 | * ``` 29 | */ 30 | export function useCounter(initialValue?: number): UseCounterReturn { 31 | const [count, setCount] = useState(initialValue ?? 0) 32 | 33 | const increment = useCallback(() => { 34 | setCount(x => x + 1) 35 | }, []) 36 | 37 | const decrement = useCallback(() => { 38 | setCount(x => x - 1) 39 | }, []) 40 | 41 | const reset = useCallback(() => { 42 | setCount(initialValue ?? 0) 43 | }, [initialValue]) 44 | 45 | return { 46 | count, 47 | increment, 48 | decrement, 49 | reset, 50 | setCount, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useDebounceValue/useDebounceValue.test.ts: -------------------------------------------------------------------------------- 1 | import { act, renderHook } from '@testing-library/react' 2 | 3 | import { useDebounceValue } from './useDebounceValue' 4 | 5 | describe('useDebounceValue()', () => { 6 | vitest.useFakeTimers() 7 | 8 | it('should debounce the value update', () => { 9 | const { result } = renderHook(() => useDebounceValue('initial', 100)) 10 | 11 | expect(result.current[0]).toBe('initial') 12 | 13 | act(() => { 14 | result.current[1]('update1') 15 | result.current[1]('update2') 16 | result.current[1]('update3') 17 | }) 18 | 19 | expect(result.current[0]).toBe('initial') 20 | 21 | // Advance timers by more than delay 22 | act(() => { 23 | vitest.advanceTimersByTime(200) 24 | }) 25 | 26 | expect(result.current[0]).toBe('update3') 27 | 28 | // Advance timers by more than delay again 29 | act(() => { 30 | vitest.advanceTimersByTime(200) 31 | }) 32 | 33 | expect(result.current[0]).toBe('update3') 34 | }) 35 | 36 | it('should handle options', () => { 37 | const delay = 500 38 | const { result } = renderHook(() => 39 | useDebounceValue('initial', delay, { leading: true }), 40 | ) 41 | 42 | expect(result.current[0]).toBe('initial') 43 | 44 | act(() => { 45 | result.current[1]('updated') 46 | }) 47 | 48 | // The debounced value should be updated immediately due to leading option 49 | expect(result.current[0]).toBe('updated') 50 | 51 | // Wait for the debounce interval to elapse 52 | vitest.advanceTimersByTime(delay) 53 | 54 | // The debounced value should not be updated again after the interval 55 | expect(result.current[0]).toBe('updated') 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /apps/www/src/lib/api.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { compileMDX } from 'next-mdx-remote/rsc' 4 | import path from 'path' 5 | import rehypePrism from 'rehype-prism-plus' 6 | import remarkGfm from 'remark-gfm' 7 | 8 | import { components } from '@/components/ui/components' 9 | import type { BaseHook } from '@/types' 10 | import fs from 'fs/promises' 11 | 12 | const SOURCE_PATH = path.resolve(process.cwd(), '..', '..', 'generated', 'docs') 13 | 14 | /** 15 | * Fetches and compiles the Markdown content for a specific hook. 16 | * @param slug The slug of the hook to fetch. 17 | * @returns Compiled MDX content for the hook. 18 | */ 19 | export const getHook = async (slug: string) => { 20 | try { 21 | const filename = path.resolve(SOURCE_PATH, 'hooks', `${slug}.md`) 22 | const source = await fs.readFile(filename, { encoding: 'utf-8' }) 23 | return await compileMDX<BaseHook>({ 24 | source, 25 | options: { 26 | parseFrontmatter: true, 27 | mdxOptions: { 28 | // @ts-ignore any 29 | rehypePlugins: [[rehypePrism]], 30 | remarkPlugins: [remarkGfm], 31 | }, 32 | }, 33 | components, 34 | }) 35 | } catch (error) { 36 | console.error(`Error fetching hook with slug '${slug}': `, error) 37 | throw error 38 | } 39 | } 40 | 41 | /** 42 | * Retrieves a list of all hooks from the JSON file. 43 | * @returns An array of BaseHook objects representing all hooks. 44 | */ 45 | export const getHookList = async () => { 46 | try { 47 | const filename = path.resolve(SOURCE_PATH, 'hooks.json') 48 | const file = await fs.readFile(filename, { encoding: 'utf-8' }) 49 | return JSON.parse(file) as BaseHook[] 50 | } catch (error) { 51 | console.error(`Error retrieving hook list: `, error) 52 | throw error 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useInterval/useInterval.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react' 2 | 3 | import { useInterval } from './useInterval' 4 | 5 | describe('useInterval()', () => { 6 | beforeEach(() => { 7 | vitest.clearAllMocks() 8 | vitest.useFakeTimers() 9 | }) 10 | 11 | it('should fire the callback function (1)', () => { 12 | const timeout = 500 13 | const callback = vitest.fn() 14 | renderHook(() => { 15 | useInterval(callback, timeout) 16 | }) 17 | vitest.advanceTimersByTime(timeout) 18 | expect(callback).toHaveBeenCalledTimes(1) 19 | }) 20 | 21 | it('should fire the callback function (2)', () => { 22 | const timeout = 500 23 | const earlyTimeout = 400 24 | const callback = vitest.fn() 25 | renderHook(() => { 26 | useInterval(callback, timeout) 27 | }) 28 | vitest.advanceTimersByTime(earlyTimeout) 29 | expect(callback).not.toHaveBeenCalled() 30 | }) 31 | 32 | it('should call set interval on start', () => { 33 | mockSetInterval() 34 | const timeout = 1200 35 | const callback = vitest.fn() 36 | renderHook(() => { 37 | useInterval(callback, timeout) 38 | }) 39 | expect(setInterval).toHaveBeenCalledTimes(1) 40 | expect(setInterval).toHaveBeenCalledWith(expect.any(Function), timeout) 41 | }) 42 | 43 | it('should call clearTimeout on unmount', () => { 44 | mockClearInterval() 45 | const callback = vitest.fn() 46 | const { unmount } = renderHook(() => { 47 | useInterval(callback, 1200) 48 | }) 49 | unmount() 50 | expect(clearInterval).toHaveBeenCalledTimes(1) 51 | }) 52 | }) 53 | 54 | function mockSetInterval() { 55 | vitest.spyOn(global, 'setInterval') 56 | } 57 | 58 | function mockClearInterval() { 59 | vitest.spyOn(global, 'clearInterval') 60 | } 61 | -------------------------------------------------------------------------------- /scripts/utils/get-hooks.js: -------------------------------------------------------------------------------- 1 | import { path, fs } from 'zx' 2 | import { camelToKebabCase } from './data-transform.js' 3 | 4 | export function getHooks() { 5 | const jsonFilePath = path.resolve('./generated/typedoc/all.json') 6 | const jsonFile = fs.readFileSync(jsonFilePath, 'utf-8') 7 | if (!jsonFile) { 8 | throw new Error( 9 | `Could not read ${jsonFilePath} file. Please run the typedoc command first.`, 10 | ) 11 | } 12 | return JSON.parse(jsonFile).children.map(child => { 13 | const name = child.name.split('/')[0] 14 | const slug = camelToKebabCase(name) 15 | const funcGroup = child.groups?.find(g => g.title === 'Functions') 16 | const typesGroup = child.groups?.filter(g => g.title === 'Type Aliases') 17 | const hookFunc = child.children?.find(c => c.id === funcGroup.children[0]) 18 | const types = typesGroup?.length ? typesGroup[0].children || [] : [] 19 | const summary = (hookFunc.signatures[0].comment?.summary || []) 20 | .map(s => s.text || '') 21 | .join('') 22 | 23 | // .reduce( 24 | // (acc, item) => { 25 | // if (item.text) { 26 | // acc += item.text 27 | // } 28 | // return acc 29 | // }, 30 | // '', 31 | // ) 32 | 33 | return { 34 | id: child.id, 35 | name, 36 | slug, 37 | path: `/react-hook/${slug}`, 38 | summary, 39 | flags: hookFunc.flags, 40 | links: { 41 | doc: `https://usehooks-ts.com/react-hook/${slug}`, 42 | github: hookFunc.sources[0].url, 43 | }, 44 | types: types.map(id => { 45 | const item = child.children.find(c => c.id === id) 46 | return { 47 | id: item.id, 48 | name: item.name, 49 | summary: item.comment?.summary[0].text, 50 | } 51 | }), 52 | } 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useBoolean/useBoolean.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react' 2 | 3 | import type { Dispatch, SetStateAction } from 'react' 4 | 5 | /** The useBoolean return type. */ 6 | type UseBooleanReturn = { 7 | /** The current boolean state value. */ 8 | value: boolean 9 | /** Function to set the boolean state directly. */ 10 | setValue: Dispatch<SetStateAction<boolean>> 11 | /** Function to set the boolean state to `true`. */ 12 | setTrue: () => void 13 | /** Function to set the boolean state to `false`. */ 14 | setFalse: () => void 15 | /** Function to toggle the boolean state. */ 16 | toggle: () => void 17 | } 18 | 19 | /** 20 | * Custom hook that handles boolean state with useful utility functions. 21 | * @param {boolean} [defaultValue] - The initial value for the boolean state (default is `false`). 22 | * @returns {UseBooleanReturn} An object containing the boolean state value and utility functions to manipulate the state. 23 | * @throws Will throw an error if `defaultValue` is an invalid boolean value. 24 | * @public 25 | * @see [Documentation](https://usehooks-ts.com/react-hook/use-boolean) 26 | * @example 27 | * ```tsx 28 | * const { value, setTrue, setFalse, toggle } = useBoolean(true); 29 | * ``` 30 | */ 31 | export function useBoolean(defaultValue = false): UseBooleanReturn { 32 | if (typeof defaultValue !== 'boolean') { 33 | throw new Error('defaultValue must be `true` or `false`') 34 | } 35 | const [value, setValue] = useState(defaultValue) 36 | 37 | const setTrue = useCallback(() => { 38 | setValue(true) 39 | }, []) 40 | 41 | const setFalse = useCallback(() => { 42 | setValue(false) 43 | }, []) 44 | 45 | const toggle = useCallback(() => { 46 | setValue(x => !x) 47 | }, []) 48 | 49 | return { value, setValue, setTrue, setFalse, toggle } 50 | } 51 | -------------------------------------------------------------------------------- /apps/www/src/components/doc-search/hits.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/navigation' 2 | import { useHits } from 'react-instantsearch' 3 | 4 | import { CommandItem } from '../ui/command' 5 | import { useCommandMenuContext } from './modal.context' 6 | import type { Hit } from './types' 7 | 8 | export function RenderHits() { 9 | const { hits, results } = useHits<Hit>() 10 | 11 | if (!results?.index) { 12 | return null 13 | } 14 | 15 | return ( 16 | <> 17 | {hits.map(hit => ( 18 | <HitComponent 19 | key={hit.objectID} 20 | hit={hit} 21 | makeUrl={slug => 22 | results.index === 'hooks' 23 | ? `/react-hook/${slug}` 24 | : `/migrate-to-v3#removed-hooks` 25 | } 26 | /> 27 | ))} 28 | </> 29 | ) 30 | } 31 | 32 | type HitProps = { 33 | hit: Hit 34 | makeUrl: (slug: string) => string 35 | } 36 | 37 | function HitComponent({ hit, makeUrl }: HitProps) { 38 | const { handleClose } = useCommandMenuContext() 39 | const router = useRouter() 40 | 41 | return ( 42 | <CommandItem 43 | className="flex flex-col [&_mark]:bg-accent [&_mark]:text-accent-foreground" 44 | onSelect={() => { 45 | handleClose() 46 | 47 | const url = makeUrl(hit.objectID) 48 | router.push(url) 49 | }} 50 | > 51 | <div 52 | className="font-mono" 53 | dangerouslySetInnerHTML={{ 54 | __html: (hit._highlightResult.name?.value || hit.name) + '()', 55 | }} 56 | /> 57 | <div 58 | className="text-sm text-muted-foreground" 59 | dangerouslySetInnerHTML={{ 60 | __html: ( 61 | hit._highlightResult.summary?.value ?? 62 | hit?.summary ?? 63 | '' 64 | ).replace('Custom hook that ', ''), 65 | }} 66 | /> 67 | </CommandItem> 68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /apps/www/src/app/(docs)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | import { DocSearch } from '@/components/doc-search' 4 | import { LeftSidebar } from '@/components/docs/left-sidebar' 5 | import { MainNav } from '@/components/main-nav' 6 | import { GitHub } from '@/components/ui/icons' 7 | import { docsConfig } from '@/config/docs' 8 | import { siteConfig } from '@/config/site' 9 | import { getHookList } from '@/lib/api' 10 | 11 | type DocsLayoutProps = { 12 | children: React.ReactNode 13 | } 14 | 15 | export default async function DocsLayout({ children }: DocsLayoutProps) { 16 | const hooks = await getHookList() 17 | 18 | return ( 19 | <> 20 | <header className="sticky top-0 z-40 w-full border-b bg-background"> 21 | <div className="container flex h-16 items-center space-x-4 sm:justify-between sm:space-x-0"> 22 | <MainNav items={docsConfig.mainNav}> 23 | <LeftSidebar items={docsConfig.sidebarNav} hooks={hooks} /> 24 | </MainNav> 25 | <div className="flex flex-1 items-center space-x-4 sm:justify-end"> 26 | <nav className="flex space-x-4"> 27 | <DocSearch /> 28 | <Link 29 | href={siteConfig.links.github} 30 | target="_blank" 31 | rel="noreferrer" 32 | className="flex" 33 | > 34 | <GitHub className="h-6 w-6 my-auto" /> 35 | <span className="sr-only">GitHub</span> 36 | </Link> 37 | </nav> 38 | </div> 39 | </div> 40 | </header> 41 | 42 | <main className="container flex-1"> 43 | <div className="flex-1 md:grid md:grid-cols-[220px_1fr] md:gap-6 lg:grid-cols-[240px_1fr] lg:gap-10"> 44 | <LeftSidebar items={docsConfig.sidebarNav} hooks={hooks} /> 45 | {children} 46 | </div> 47 | </main> 48 | </> 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://typedoc.org/schema.json", 3 | 4 | // Essentials 5 | "name": "usehooks-ts", 6 | "tsconfig": "packages/usehooks-ts/tsconfig.json", 7 | "jsDocCompatibility": true, 8 | "entryPoints": ["packages/usehooks-ts/src/**/*.ts"], 9 | "entryPointStrategy": "resolve", 10 | "json": "./generated/typedoc/all.json", 11 | "out": "./generated/typedoc", 12 | "readme": "none", 13 | 14 | // Exclude 15 | "exclude": [ 16 | "packages/usehooks-ts/src/**/demo.*", 17 | "packages/usehooks-ts/src/**/test.*", 18 | "packages/usehooks-ts/src/**/index.ts" 19 | ], 20 | "externalPattern": ["**/node_modules/**"], 21 | "excludeExternals": true, 22 | "excludePrivate": true, 23 | "excludeProtected": true, 24 | "excludeInternal": true, 25 | "excludeNotDocumented": true, 26 | "excludeReferences": true, 27 | "excludeTags": [ 28 | "@override", 29 | "@virtual", 30 | "@privateRemarks", 31 | "@satisfies", 32 | "@overload", 33 | "@example", 34 | "@see" 35 | ], 36 | 37 | // Plugins 38 | "plugin": [ 39 | "typedoc-plugin-mdn-links", 40 | "typedoc-plugin-markdown", 41 | "typedoc-plugin-missing-exports" 42 | ], 43 | 44 | // Validation 45 | "validation": { 46 | "notExported": true, 47 | "invalidLink": true, 48 | "notDocumented": false 49 | }, 50 | // Emit warnings for any tags not listed here 51 | "blockTags": ["@param", "@returns", "@see", "@example", "@template"], 52 | 53 | // Markdown and styles 54 | "allReflectionsHaveOwnDocument": true, 55 | "hidePageTitle": true, 56 | "hideInPageTOC": true, 57 | "hideGenerator": true, 58 | "hideBreadcrumbs": true, 59 | "hideParameterTypesInTitle": true, 60 | "navigation": { 61 | "includeCategories": false, 62 | "includeGroups": false, 63 | "includeFolders": false 64 | }, 65 | "sort": ["alphabetical"], 66 | "preserveLinkText": true, 67 | "placeInternalsInOwningModule": true 68 | } 69 | -------------------------------------------------------------------------------- /apps/www/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "www", 3 | "version": "1.0.4", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "next dev", 8 | "build": "pnpm generate-doc && next build && pnpm generate-sitemap", 9 | "generate-sitemap": "rimraf public/sitemap.xml public/robots.txt && next-sitemap --config ./next-sitemap.config.js", 10 | "start": "next start", 11 | "lint": "next lint && tsc --noEmit", 12 | "clean": "rimraf *.tsbuildinfo .next .turbo", 13 | "generate-doc": "cd ../.. && pnpm generate-doc && cd -" 14 | }, 15 | "dependencies": { 16 | "@next/third-parties": "^14.1.0", 17 | "@radix-ui/react-dialog": "^1.0.5", 18 | "@radix-ui/react-dropdown-menu": "^2.0.6", 19 | "@radix-ui/react-slot": "^1.0.2", 20 | "@t3-oss/env-nextjs": "^0.9.2", 21 | "@types/voca": "^1.4.1", 22 | "algoliasearch": "^4.22.1", 23 | "class-variance-authority": "^0.7.0", 24 | "clsx": "^2.1.0", 25 | "cmdk": "^1.0.0", 26 | "date-fns": "^3.3.1", 27 | "gray-matter": "^4.0.3", 28 | "lucide-react": "^0.364.0", 29 | "next": "14.1.4", 30 | "next-mdx-remote": "^4.4.1", 31 | "react": "18.2.0", 32 | "react-dom": "18.2.0", 33 | "react-instantsearch": "^7.6.0", 34 | "rehype-prism-plus": "^2.0.0", 35 | "remark-gfm": "^3.0.1", 36 | "schema-dts": "^1.1.2", 37 | "tailwind-merge": "^2.2.1", 38 | "tailwindcss": "3.4.3", 39 | "tailwindcss-animate": "^1.0.7", 40 | "usehooks-ts": "workspace:*", 41 | "voca": "^1.4.1", 42 | "zod": "^3.22.4" 43 | }, 44 | "devDependencies": { 45 | "@tailwindcss/line-clamp": "^0.4.4", 46 | "@tailwindcss/typography": "^0.5.10", 47 | "@types/node": "20.12.2", 48 | "@types/react": "18.2.73", 49 | "@types/react-dom": "18.2.23", 50 | "autoprefixer": "10.4.19", 51 | "eslint-config-custom": "workspace:*", 52 | "eslint-config-next": "14.1.4", 53 | "next-sitemap": "^4.2.3", 54 | "postcss": "8.4.38", 55 | "typescript": "5.4.3" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useEventCallback/useEventCallback.test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, render, renderHook, screen } from '@testing-library/react' 2 | import type { Mock } from 'vitest' 3 | 4 | import { useEventCallback } from './useEventCallback' 5 | 6 | describe('useEventCallback()', () => { 7 | it('should not call the callback during render', () => { 8 | const fn = vi.fn() 9 | const { result } = renderHook(() => useEventCallback(fn)) 10 | 11 | render(<button onClick={result.current}>Click me</button>) 12 | 13 | expect(fn).not.toHaveBeenCalled() 14 | }) 15 | 16 | it('should call the callback when the event is triggered', () => { 17 | const fn = vi.fn() 18 | const { result } = renderHook(() => useEventCallback(fn)) 19 | 20 | render(<button onClick={result.current}>Click me</button>) 21 | 22 | fireEvent.click(screen.getByText('Click me')) 23 | 24 | expect(fn).toHaveBeenCalled() 25 | }) 26 | 27 | it('should be typed accordingly', () => { 28 | const fn1: Mock<[React.MouseEvent<HTMLButtonElement>], void> = vi.fn() 29 | const fn1Result = renderHook(() => useEventCallback(fn1)) 30 | 31 | expectTypeOf(fn1Result.result.current).toEqualTypeOf< 32 | (event: React.MouseEvent<HTMLButtonElement>) => void 33 | >() 34 | 35 | const fn2 = undefined as 36 | | Mock<[React.MouseEvent<HTMLButtonElement>], void> 37 | | undefined 38 | const fn2Result = renderHook(() => useEventCallback(fn2)) 39 | 40 | expectTypeOf(fn2Result.result.current).toEqualTypeOf< 41 | ((event: React.MouseEvent<HTMLButtonElement>) => void) | undefined 42 | >() 43 | }) 44 | 45 | it('should allow to pass optional callback without errors', () => { 46 | const optionalFn = undefined as 47 | | ((event: React.MouseEvent<HTMLButtonElement>) => void) 48 | | undefined 49 | 50 | const { result } = renderHook(() => useEventCallback(optionalFn)) 51 | 52 | render(<button onClick={result.current}>Click me</button>) 53 | 54 | fireEvent.click(screen.getByText('Click me')) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /turbo/generators/config.cts: -------------------------------------------------------------------------------- 1 | import type { PlopTypes } from '@turbo/gen' 2 | import { format } from 'date-fns' 3 | import path from 'path' 4 | 5 | export default function generator(plop: PlopTypes.NodePlopAPI): void { 6 | const usehooksSrcPath = path.resolve('packages/usehooks-ts/src') 7 | plop.setGenerator('hook', { 8 | description: 'Create a post', 9 | prompts: [ 10 | { 11 | type: 'input', 12 | name: 'name', 13 | message: 'post name please (eg: "use test")', 14 | }, 15 | ], 16 | actions: [ 17 | // Create the hook file itself 18 | { 19 | type: 'add', 20 | path: usehooksSrcPath + '/{{camelCase name}}/{{camelCase name}}.ts', 21 | templateFile: 'templates/hook/hook.ts.hbs', 22 | }, 23 | 24 | // Create the test file 25 | { 26 | type: 'add', 27 | path: usehooksSrcPath + '/{{camelCase name}}/{{camelCase name}}.test.ts', 28 | templateFile: 'templates/hook/hook.test.ts.hbs', 29 | }, 30 | 31 | // Create the markdown file to present the hook (doc) 32 | { 33 | data: { 34 | date: format(new Date(), 'yyyy-MM-dd'), 35 | }, 36 | type: 'add', 37 | path: usehooksSrcPath + '/{{camelCase name}}/{{camelCase name}}.md', 38 | templateFile: 'templates/hook/hook.mdx.hbs', 39 | }, 40 | 41 | // Create the demo react component file 42 | { 43 | type: 'add', 44 | path: usehooksSrcPath + '/{{camelCase name}}/{{camelCase name}}.demo.tsx', 45 | templateFile: 'templates/hook/hook.demo.tsx.hbs', 46 | }, 47 | 48 | // Create the hook's index file 49 | { 50 | type: 'add', 51 | path: usehooksSrcPath + '/{{camelCase name}}/index.ts', 52 | templateFile: 'templates/hook/index.ts.hbs', 53 | }, 54 | 55 | // Update the global hooks index file 56 | { 57 | type: 'append', 58 | path: usehooksSrcPath + '/index.ts', 59 | templateFile: 'templates/index.ts.hbs', 60 | }, 61 | ], 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.tabSize": 2, 4 | "files.encoding": "utf8", 5 | "files.trimTrailingWhitespace": true, 6 | "files.insertFinalNewline": true, 7 | "search.exclude": { 8 | "public/**": true, 9 | "node_modules/**": true, 10 | "coverage/**": true, 11 | "dist/**": true, 12 | "generated/**": true, 13 | ".cache/**": true, 14 | ".git/**": true, 15 | "**/package-lock.json": true, 16 | "**/pnpm-lock.yaml": true 17 | }, 18 | "eslint.validate": [ 19 | "javascript", 20 | "javascriptreact", 21 | "typescript", 22 | "typescriptreact" 23 | ], 24 | "eslint.format.enable": true, 25 | "editor.codeActionsOnSave": { 26 | "source.fixAll.eslint": "explicit", 27 | "source.fixAll": "explicit" 28 | }, 29 | "editor.defaultFormatter": "esbenp.prettier-vscode", 30 | "cSpell.words": [ 31 | "algolia", 32 | "clsx", 33 | "cmdk", 34 | "contentlayer", 35 | "fira", 36 | "frontmatter", 37 | "gtag", 38 | "juliencrn", 39 | "lucide", 40 | "nextjs", 41 | "okaidia", 42 | "prismjs", 43 | "rehype", 44 | "tailwindcss", 45 | "UMAMI", 46 | "usehooks" 47 | ], 48 | "eslint.workingDirectories": [ 49 | { 50 | "directory": "apps/www", 51 | "changeProcessCWD": true 52 | }, 53 | { 54 | "directory": "packages/eslint-config-custom", 55 | "changeProcessCWD": true 56 | }, 57 | { 58 | "directory": "packages/usehooks-ts", 59 | "changeProcessCWD": true 60 | } 61 | ], 62 | "[jsonc]": { 63 | "editor.defaultFormatter": "esbenp.prettier-vscode" 64 | }, 65 | "[json]": { 66 | "editor.defaultFormatter": "esbenp.prettier-vscode" 67 | }, 68 | "[yaml]": { 69 | "editor.defaultFormatter": "esbenp.prettier-vscode" 70 | }, 71 | "[typescript]": { 72 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 73 | }, 74 | "[typescriptreact]": { 75 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 76 | }, 77 | "files.associations": { "*.json": "jsonc" } 78 | } 79 | -------------------------------------------------------------------------------- /scripts/update-algolia-index.js: -------------------------------------------------------------------------------- 1 | import { getHooks } from './utils/get-hooks.js' 2 | 3 | import algoliasearch from 'algoliasearch' 4 | import { env } from './env.js' 5 | 6 | // Prepare the algolia records from the hooks 7 | const records = getHooks().map(({ name, slug, summary }) => ({ 8 | objectID: slug, 9 | name, 10 | summary, 11 | })) 12 | 13 | // Connect and authenticate with your Algolia app 14 | const client = algoliasearch(env.ALGOLIA_APP_ID, env.ALGOLIA_ADMIN_KEY) 15 | 16 | // Create a new index 17 | const index = client.initIndex('hooks') 18 | 19 | // Set the index settings 20 | index.setSettings({ 21 | camelCaseAttributes: ['name'], 22 | searchableAttributes: ['name', 'objectID', 'summary'], 23 | hitsPerPage: 1000, 24 | }) 25 | 26 | // Add or update the records 27 | index 28 | .saveObjects(records) 29 | .then(({ objectIDs }) => { 30 | console.log({ count: objectIDs.length, objectIDs }) 31 | }) 32 | .catch(err => { 33 | console.error(err) 34 | process.exit(1) 35 | }) 36 | 37 | // Include removed hooks 38 | const removedHooks = [ 39 | { objectID: 'use-debounce', name: 'useDebounce' }, 40 | { objectID: 'use-fetch', name: 'useFetch' }, 41 | { objectID: 'use-element-size', name: 'useElementSize' }, 42 | { objectID: 'use-locked-body', name: 'useLockedBody' }, 43 | { objectID: 'use-is-first-render', name: 'useIsFirstRender' }, 44 | { objectID: 'use-ssr', name: 'useSsr' }, 45 | { objectID: 'use-effect-once', name: 'useEffectOnce' }, 46 | { objectID: 'use-update-effect', name: 'useUpdateEffect' }, 47 | { objectID: 'use-image-on-load', name: 'useImageOnLoad' }, 48 | ] 49 | 50 | const removedIndex = client.initIndex('removed-hooks') 51 | 52 | removedIndex.setSettings({ 53 | camelCaseAttributes: ['name'], 54 | searchableAttributes: ['name', 'objectID'], 55 | hitsPerPage: 1000, 56 | }) 57 | 58 | removedIndex 59 | .saveObjects(removedHooks) 60 | .then(({ objectIDs }) => { 61 | console.log({ count: objectIDs.length, objectIDs }) 62 | }) 63 | .catch(err => { 64 | console.error(err) 65 | process.exit(1) 66 | }) 67 | -------------------------------------------------------------------------------- /apps/www/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { Slot } from '@radix-ui/react-slot' 4 | import type { VariantProps } from 'class-variance-authority' 5 | import { cva } from 'class-variance-authority' 6 | 7 | import { cn } from '@/lib/utils' 8 | 9 | const buttonVariants = cva( 10 | 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background', 11 | { 12 | variants: { 13 | variant: { 14 | default: 'bg-primary text-primary-foreground hover:bg-primary/90', 15 | destructive: 16 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90', 17 | outline: 18 | 'border border-input hover:bg-accent hover:text-accent-foreground', 19 | secondary: 20 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80', 21 | ghost: 'hover:bg-accent hover:text-accent-foreground', 22 | link: 'underline-offset-4 hover:underline text-primary', 23 | }, 24 | size: { 25 | default: 'h-10 py-2 px-4', 26 | sm: 'h-9 px-3 rounded-md', 27 | lg: 'h-11 px-8 rounded-md', 28 | icon: 'h-10 w-10', 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: 'default', 33 | size: 'default', 34 | }, 35 | }, 36 | ) 37 | 38 | export type ButtonProps = { 39 | asChild?: boolean 40 | } & React.ButtonHTMLAttributes<HTMLButtonElement> & 41 | VariantProps<typeof buttonVariants> 42 | 43 | const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : 'button' 46 | return ( 47 | <Comp 48 | className={cn(buttonVariants({ variant, size, className }))} 49 | ref={ref} 50 | {...props} 51 | /> 52 | ) 53 | }, 54 | ) 55 | Button.displayName = 'Button' 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useCopyToClipboard/useCopyToClipboard.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react' 2 | 3 | /** 4 | * The copied text as `string` or `null` if nothing has been copied yet. 5 | */ 6 | type CopiedValue = string | null 7 | 8 | /** 9 | * Function to copy text to the clipboard. 10 | * @param text - The text to copy to the clipboard. 11 | * @returns {Promise<boolean>} A promise that resolves to `true` if the text was copied successfully, or `false` otherwise. 12 | */ 13 | type CopyFn = (text: string) => Promise<boolean> 14 | 15 | /** 16 | * Custom hook that copies text to the clipboard using the [`Clipboard API`](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API). 17 | * @returns {[CopiedValue, CopyFn]} An tuple containing the copied text and a function to copy text to the clipboard. 18 | * @public 19 | * @see [Documentation](https://usehooks-ts.com/react-hook/use-copy-to-clipboard) 20 | * @example 21 | * ```tsx 22 | * const [copiedText, copyToClipboard] = useCopyToClipboard(); 23 | * const textToCopy = 'Hello, world!'; 24 | * 25 | * // Attempt to copy text to the clipboard 26 | * copyToClipboard(textToCopy) 27 | * .then(success => { 28 | * if (success) { 29 | * console.log(`Text "${textToCopy}" copied to clipboard successfully.`); 30 | * } else { 31 | * console.error('Failed to copy text to clipboard.'); 32 | * } 33 | * }); 34 | * ``` 35 | */ 36 | export function useCopyToClipboard(): [CopiedValue, CopyFn] { 37 | const [copiedText, setCopiedText] = useState<CopiedValue>(null) 38 | 39 | const copy: CopyFn = useCallback(async text => { 40 | if (!navigator?.clipboard) { 41 | console.warn('Clipboard not supported') 42 | return false 43 | } 44 | 45 | // Try to save to clipboard then save it in the state if worked 46 | try { 47 | await navigator.clipboard.writeText(text) 48 | setCopiedText(text) 49 | return true 50 | } catch (error) { 51 | console.warn('Copy failed', error) 52 | setCopiedText(null) 53 | return false 54 | } 55 | }, []) 56 | 57 | return [copiedText, copy] 58 | } 59 | -------------------------------------------------------------------------------- /scripts/utils/generate-doc-files.js: -------------------------------------------------------------------------------- 1 | import { fs, path } from 'zx' 2 | import { 3 | removeDefinedInSections, 4 | removeEslintDisableComments, 5 | removeFirstLine, 6 | removeJSDocComments, 7 | transformImports, 8 | replaceRelativePaths, 9 | } from './data-transform.js' 10 | import { 11 | getCodeData, 12 | getDemoData, 13 | getHookDocData, 14 | getTypeAliasesData, 15 | } from './get-markdown-data.js' 16 | 17 | export function generateDocFiles(hook) { 18 | const [hookDoc] = getHookDocData(hook) 19 | .map(removeFirstLine) 20 | .map(removeDefinedInSections) 21 | .map(replaceRelativePaths) 22 | // .map(data => data.trim()) 23 | 24 | const typeAliases = getTypeAliasesData(hook) 25 | .map(removeFirstLine) 26 | .map(removeDefinedInSections) 27 | .map(replaceRelativePaths) 28 | 29 | const [demo] = getDemoData(hook) 30 | .map(removeJSDocComments) 31 | .map(removeEslintDisableComments) 32 | .map(transformImports) 33 | 34 | const [code] = getCodeData(hook) 35 | .map(removeJSDocComments) 36 | .map(removeEslintDisableComments) 37 | .map(transformImports) 38 | .map(data => data.trim()) 39 | 40 | const hookHighlightIndexes = demo 41 | .split('\n') 42 | .map((line, index) => { 43 | if (line.startsWith('import')) return null 44 | if (!line.includes(hook.name)) return null 45 | return index + 1 46 | }) 47 | .filter(Boolean) 48 | 49 | // Template 50 | const data = `--- 51 | name: ${hook.name} 52 | slug: ${hook.slug} 53 | path: /react-hook/${hook.slug} 54 | summary: ${hook.summary} 55 | --- 56 | 57 | ${hook.summary} 58 | 59 | ## Usage 60 | 61 | \`\`\`tsx showLineNumbers {${hookHighlightIndexes.join(',')}} 62 | ${demo.trim()} 63 | \`\`\` 64 | 65 | ## API 66 | 67 | ${hookDoc} 68 | 69 | ${typeAliases.length > 0 ? '### Type aliases\n\n' + typeAliases.join('\n') + '\n' : ''} 70 | 71 | ## Hook 72 | 73 | \`\`\`ts showLineNumbers 74 | ${code} 75 | \`\`\` 76 | ` 77 | 78 | // Write the file 79 | const file = path.resolve(`./generated/docs/hooks/${hook.slug}.md`) 80 | const writeStream = fs.createWriteStream(file) 81 | writeStream.write(data) 82 | writeStream.end() 83 | } 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workspace", 3 | "private": true, 4 | "description": "React hook library, ready to use, written in Typescript.", 5 | "author": "Julien CARON <juliencaron@protonmail.com>", 6 | "homepage": "https://usehooks-ts.com", 7 | "module": "true", 8 | "type": "module", 9 | "keywords": [ 10 | "typescript", 11 | "react", 12 | "hooks" 13 | ], 14 | "license": "MIT", 15 | "scripts": { 16 | "preinstall": "npx only-allow pnpm", 17 | "dev": "turbo run dev", 18 | "build": "turbo run build", 19 | "test": "turbo run test", 20 | "clean": "rimraf .turbo generated && turbo run clean", 21 | "format": "prettier --write \"**/*.{json,md,mdx,css,scss,yaml,yml}\" --ignore-path .prettierignore", 22 | "lint": "turbo run lint", 23 | "update-testing-issue": "zx ./scripts/update-testing-issue.js", 24 | "update-algolia-index": "zx ./scripts/update-algolia-index.js", 25 | "gen-hook": "turbo gen hook --config \"turbo/generators/config.cts\" && pnpm format", 26 | "changeset": "npx changeset", 27 | "changeset-version": "npx changeset version", 28 | "changeset-publish": "npx changeset publish", 29 | "generate-doc": "zx ./scripts/generate-doc.js" 30 | }, 31 | "resolutions": { 32 | "typescript": "^5.3.3" 33 | }, 34 | "devDependencies": { 35 | "@changesets/cli": "^2.27.1", 36 | "@turbo/gen": "^1.12.4", 37 | "@t3-oss/env-core": "^0.9.2", 38 | "all-contributors-cli": "^6.26.1", 39 | "algoliasearch": "^4.22.1", 40 | "date-fns": "^3.3.1", 41 | "dotenv": "16.4.5", 42 | "eslint": "^8.56.0", 43 | "prettier": "^3.2.5", 44 | "rimraf": "^5.0.5", 45 | "turbo": "^1.12.4", 46 | "typedoc": "^0.25.9", 47 | "typedoc-plugin-markdown": "^3.17.1", 48 | "typedoc-plugin-mdn-links": "^3.1.17", 49 | "typedoc-plugin-missing-exports": "^2.2.0", 50 | "zod": "3.22.4", 51 | "zx": "^7.2.3" 52 | }, 53 | "engines": { 54 | "node": ">=18.17.0" 55 | }, 56 | "repository": { 57 | "type": "git", 58 | "url": "https://github.com/juliencrn/usehooks-ts" 59 | }, 60 | "bugs": { 61 | "url": "https://github.com/juliencrn/usehooks-ts/issues" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/usehooks-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "usehooks-ts", 3 | "private": false, 4 | "version": "3.1.1", 5 | "description": "React hook library, ready to use, written in Typescript.", 6 | "author": "Julien CARON <juliencaron@protonmail.com>", 7 | "homepage": "https://usehooks-ts.com", 8 | "keywords": [ 9 | "typescript", 10 | "react", 11 | "hooks" 12 | ], 13 | "license": "MIT", 14 | "type": "module", 15 | "main": "./dist/index.js", 16 | "types": "./dist/index.d.ts", 17 | "exports": { 18 | "./package.json": "./package.json", 19 | ".": { 20 | "import": { 21 | "types": "./dist/index.d.ts", 22 | "default": "./dist/index.js" 23 | }, 24 | "require": { 25 | "types": "./dist/index.d.cts", 26 | "default": "./dist/index.cjs" 27 | } 28 | } 29 | }, 30 | "sideEffects": false, 31 | "scripts": { 32 | "build": "tsup", 33 | "dev": "tsup --watch", 34 | "test": "vitest run", 35 | "test:watch": "vitest", 36 | "clean": "rimraf dist .turbo *.tsbuildinfo", 37 | "lint": "eslint './src/**/*.{ts,tsx}' && tsc --noEmit" 38 | }, 39 | "devDependencies": { 40 | "@juggle/resize-observer": "^3.4.0", 41 | "@testing-library/jest-dom": "^6.4.2", 42 | "@testing-library/react": "^14.2.1", 43 | "@types/lodash.debounce": "^4.0.9", 44 | "@types/node": "^20.11.19", 45 | "@types/react": "18.2.73", 46 | "eslint-config-custom": "workspace:*", 47 | "eslint-plugin-jsdoc": "^48.1.0", 48 | "eslint-plugin-tree-shaking": "^1.12.1", 49 | "jsdom": "^24.0.0", 50 | "react": "18.2.0", 51 | "tsup": "^8.0.2", 52 | "typescript": "^5.3.3", 53 | "vitest": "^1.3.1" 54 | }, 55 | "dependencies": { 56 | "lodash.debounce": "^4.0.8" 57 | }, 58 | "peerDependencies": { 59 | "react": "^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" 60 | }, 61 | "engines": { 62 | "node": ">=16.15.0" 63 | }, 64 | "files": [ 65 | "dist" 66 | ], 67 | "repository": { 68 | "type": "git", 69 | "url": "https://github.com/juliencrn/usehooks-ts" 70 | }, 71 | "bugs": { 72 | "url": "https://github.com/juliencrn/usehooks-ts/issues" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useResizeObserver/useResizeObserver.test.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */ 2 | import { ResizeObserver } from '@juggle/resize-observer' 3 | import { renderHook } from '@testing-library/react' 4 | 5 | import { useResizeObserver } from './useResizeObserver' 6 | 7 | describe('useResizeObserver()', () => { 8 | beforeEach(() => { 9 | // Mock the ResizeObserver 10 | window.ResizeObserver = ResizeObserver 11 | }) 12 | 13 | afterEach(() => { 14 | vitest.restoreAllMocks() 15 | }) 16 | 17 | it('should return initial undefined sizes', () => { 18 | const ref = { current: document.createElement('div') } 19 | const { result } = renderHook(() => 20 | useResizeObserver({ 21 | ref, 22 | }), 23 | ) 24 | 25 | expect(result.current.width).toBeUndefined() 26 | expect(result.current.height).toBeUndefined() 27 | }) 28 | 29 | it.skip('should return the observed element sizes', () => { 30 | const ref = { current: document.createElement('div') } 31 | const { result } = renderHook(() => 32 | useResizeObserver({ 33 | ref, 34 | }), 35 | ) 36 | 37 | // TODO: Mock the observed element's size 38 | 39 | expect(result.current.width).toBe(100) 40 | expect(result.current.height).toBe(100) 41 | }) 42 | 43 | it.skip('should update size when element is resized', () => { 44 | const ref = { current: document.createElement('div') } 45 | const { result } = renderHook(() => 46 | useResizeObserver({ 47 | ref, 48 | }), 49 | ) 50 | 51 | // TODO: Mock the observed element's size 52 | 53 | expect(result.current.width).toBe(300) 54 | expect(result.current.height).toBe(200) 55 | }) 56 | 57 | it.skip('should use onResize callback to update the size', () => { 58 | const ref = { current: document.createElement('div') } 59 | const onResize = vitest.fn() 60 | renderHook(() => 61 | useResizeObserver({ 62 | ref, 63 | onResize, 64 | }), 65 | ) 66 | 67 | // TODO: Mock the observed element's size 68 | 69 | expect(onResize).toHaveBeenCalledWith({ width: 200, height: 200 }) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /apps/www/src/components/main-nav.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | 5 | import Link from 'next/link' 6 | import { useSelectedLayoutSegment } from 'next/navigation' 7 | 8 | import { MobileNav } from '@/components/mobile-nav' 9 | import { Close, Logo } from '@/components/ui/icons' 10 | import { siteConfig } from '@/config/site' 11 | import { cn } from '@/lib/utils' 12 | import type { MainNavItem } from '@/types' 13 | 14 | type MainNavProps = { 15 | items?: MainNavItem[] 16 | children?: React.ReactNode 17 | } 18 | 19 | export function MainNav({ items, children }: MainNavProps) { 20 | const segment = useSelectedLayoutSegment() 21 | const [showMobileMenu, setShowMobileMenu] = React.useState<boolean>(false) 22 | 23 | return ( 24 | <div className="flex gap-6 md:gap-10"> 25 | <Link href="/" className="hidden items-center space-x-2 md:flex"> 26 | <Logo /> 27 | <span className="hidden font-bold sm:inline-block"> 28 | {siteConfig.name} 29 | </span> 30 | </Link> 31 | {items?.length ? ( 32 | <nav className="hidden gap-6 md:flex"> 33 | {items?.map((item, index) => ( 34 | <Link 35 | key={index} 36 | href={item.disabled ? '#' : item.href} 37 | className={cn( 38 | 'flex items-center text-lg font-medium transition-colors hover:text-foreground/80 sm:text-sm', 39 | item.href.startsWith(`/${segment}`) 40 | ? 'text-foreground' 41 | : 'text-foreground/60', 42 | item.disabled && 'cursor-not-allowed opacity-80', 43 | )} 44 | > 45 | {item.title} 46 | </Link> 47 | ))} 48 | </nav> 49 | ) : null} 50 | <button 51 | className="flex items-center space-x-2 md:hidden" 52 | onClick={() => { 53 | setShowMobileMenu(!showMobileMenu) 54 | }} 55 | > 56 | {showMobileMenu ? <Close /> : <Logo />} 57 | <span className="font-bold">Menu</span> 58 | </button> 59 | {showMobileMenu && items && ( 60 | <MobileNav items={items}>{children}</MobileNav> 61 | )} 62 | </div> 63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useStep/useStep.test.ts: -------------------------------------------------------------------------------- 1 | import { act, renderHook } from '@testing-library/react' 2 | 3 | import { useStep } from './useStep' 4 | 5 | describe('useStep()', () => { 6 | it('should use step', () => { 7 | const { result } = renderHook(() => useStep(2)) 8 | 9 | expect(result.current[0]).toBe(1) 10 | expect(typeof result.current[1].goToNextStep).toBe('function') 11 | expect(typeof result.current[1].goToPrevStep).toBe('function') 12 | expect(typeof result.current[1].setStep).toBe('function') 13 | expect(typeof result.current[1].reset).toBe('function') 14 | expect(typeof result.current[1].canGoToNextStep).toBe('boolean') 15 | expect(typeof result.current[1].canGoToPrevStep).toBe('boolean') 16 | }) 17 | 18 | it('should increment step', () => { 19 | const { result } = renderHook(() => useStep(2)) 20 | 21 | act(() => { 22 | result.current[1].goToNextStep() 23 | }) 24 | 25 | expect(result.current[0]).toBe(2) 26 | }) 27 | 28 | it('should decrement step', () => { 29 | const { result } = renderHook(() => useStep(2)) 30 | 31 | act(() => { 32 | result.current[1].setStep(2) 33 | }) 34 | 35 | act(() => { 36 | result.current[1].goToPrevStep() 37 | }) 38 | 39 | expect(result.current[0]).toBe(1) 40 | }) 41 | 42 | it('should reset step', () => { 43 | const { result } = renderHook(() => useStep(2)) 44 | 45 | act(() => { 46 | result.current[1].reset() 47 | }) 48 | 49 | expect(result.current[0]).toBe(1) 50 | }) 51 | 52 | it('should set step', () => { 53 | const { result } = renderHook(() => useStep(3)) 54 | 55 | const newStep = 2 56 | 57 | act(() => { 58 | result.current[1].setStep(newStep) 59 | }) 60 | 61 | expect(result.current[0]).toBe(newStep) 62 | }) 63 | 64 | it('should return if prev step is available', () => { 65 | const { result } = renderHook(() => useStep(2)) 66 | 67 | act(() => { 68 | result.current[1].setStep(2) 69 | }) 70 | 71 | expect(result.current[1].canGoToPrevStep).toBe(true) 72 | }) 73 | 74 | it('should return if next step is available', () => { 75 | const { result } = renderHook(() => useStep(2)) 76 | 77 | expect(result.current[1].canGoToNextStep).toBe(true) 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useOnClickOutside/useOnClickOutside.ts: -------------------------------------------------------------------------------- 1 | import type { RefObject } from 'react' 2 | 3 | import { useEventListener } from '../useEventListener' 4 | 5 | /** Supported event types. */ 6 | type EventType = 7 | | 'mousedown' 8 | | 'mouseup' 9 | | 'touchstart' 10 | | 'touchend' 11 | | 'focusin' 12 | | 'focusout' 13 | 14 | /** 15 | * Custom hook that handles clicks outside a specified element. 16 | * @template T - The type of the element's reference. 17 | * @param {RefObject<T> | RefObject<T>[]} ref - The React ref object(s) representing the element(s) to watch for outside clicks. 18 | * @param {(event: MouseEvent | TouchEvent | FocusEvent) => void} handler - The callback function to be executed when a click outside the element occurs. 19 | * @param {EventType} [eventType] - The mouse event type to listen for (optional, default is 'mousedown'). 20 | * @param {?AddEventListenerOptions} [eventListenerOptions] - The options object to be passed to the `addEventListener` method (optional). 21 | * @returns {void} 22 | * @public 23 | * @see [Documentation](https://usehooks-ts.com/react-hook/use-on-click-outside) 24 | * @example 25 | * ```tsx 26 | * const containerRef = useRef(null); 27 | * useOnClickOutside([containerRef], () => { 28 | * // Handle clicks outside the container. 29 | * }); 30 | * ``` 31 | */ 32 | export function useOnClickOutside<T extends HTMLElement = HTMLElement>( 33 | ref: RefObject<T> | RefObject<T>[], 34 | handler: (event: MouseEvent | TouchEvent | FocusEvent) => void, 35 | eventType: EventType = 'mousedown', 36 | eventListenerOptions: AddEventListenerOptions = {}, 37 | ): void { 38 | useEventListener( 39 | eventType, 40 | event => { 41 | const target = event.target as Node 42 | 43 | // Do nothing if the target is not connected element with document 44 | if (!target || !target.isConnected) { 45 | return 46 | } 47 | 48 | const isOutside = Array.isArray(ref) 49 | ? ref 50 | .filter(r => Boolean(r.current)) 51 | .every(r => r.current && !r.current.contains(target)) 52 | : ref.current && !ref.current.contains(target) 53 | 54 | if (isOutside) { 55 | handler(event) 56 | } 57 | }, 58 | undefined, 59 | eventListenerOptions, 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /packages/usehooks-ts/src/useCounter/useCounter.test.ts: -------------------------------------------------------------------------------- 1 | import { act, renderHook } from '@testing-library/react' 2 | 3 | import { useCounter } from './useCounter' 4 | 5 | describe('useCounter()', () => { 6 | it('should use counter', () => { 7 | const { result } = renderHook(() => useCounter()) 8 | 9 | expect(result.current.count).toBe(0) 10 | expect(typeof result.current.increment).toBe('function') 11 | expect(typeof result.current.decrement).toBe('function') 12 | expect(typeof result.current.reset).toBe('function') 13 | expect(typeof result.current.setCount).toBe('function') 14 | }) 15 | 16 | it('should increment counter', () => { 17 | const { result } = renderHook(() => useCounter()) 18 | 19 | act(() => { 20 | result.current.increment() 21 | }) 22 | 23 | expect(result.current.count).toBe(1) 24 | }) 25 | 26 | it('should decrement counter', () => { 27 | const { result } = renderHook(() => useCounter()) 28 | 29 | act(() => { 30 | result.current.decrement() 31 | }) 32 | 33 | expect(result.current.count).toBe(-1) 34 | }) 35 | 36 | it('should default value works', () => { 37 | const { result } = renderHook(() => useCounter(3)) 38 | 39 | expect(result.current.count).toBe(3) 40 | }) 41 | 42 | it('should decrement counter with default value', () => { 43 | const { result } = renderHook(() => useCounter(3)) 44 | 45 | act(() => { 46 | result.current.decrement() 47 | }) 48 | 49 | expect(result.current.count).toBe(2) 50 | }) 51 | 52 | it('should set counter', () => { 53 | const { result } = renderHook(() => useCounter()) 54 | 55 | act(() => { 56 | result.current.setCount(5) 57 | }) 58 | 59 | expect(result.current.count).toBe(5) 60 | }) 61 | 62 | it('should set counter with prev value', () => { 63 | const { result } = renderHook(() => useCounter(5)) 64 | 65 | act(() => { 66 | result.current.setCount(x => x + 2) 67 | }) 68 | 69 | expect(result.current.count).toBe(7) 70 | }) 71 | 72 | it('should reset counter', () => { 73 | const { result } = renderHook(() => useCounter(0)) 74 | 75 | act(() => { 76 | result.current.increment() 77 | result.current.reset() 78 | }) 79 | 80 | expect(result.current.count).toBe(0) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /apps/www/src/components/docs/left-sidebar.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import Link from 'next/link' 4 | import { usePathname } from 'next/navigation' 5 | 6 | import { cn, mapHookToNavLink } from '@/lib/utils' 7 | import type { BaseHook, SidebarNavItem } from '@/types' 8 | 9 | type DocsSidebarNavProps = { 10 | items: SidebarNavItem[] 11 | hooks: BaseHook[] 12 | } 13 | 14 | export function LeftSidebar(props: DocsSidebarNavProps) { 15 | const pathname = usePathname() 16 | const items = [ 17 | ...props.items, 18 | { title: 'Hooks', items: props.hooks.map(mapHookToNavLink) }, 19 | ] 20 | 21 | if (!items.length) { 22 | return null 23 | } 24 | 25 | return ( 26 | <aside className="fixed top-16 z-30 hidden h-[calc(100vh-4rem-1px)] w-full shrink-0 overflow-y-auto border-r py-6 pr-2 md:sticky md:block lg:py-10"> 27 | {items.map((item, index) => ( 28 | <div key={index} className={'pb-8'}> 29 | <h4 className="mb-1 rounded-md px-2 py-1 text-sm font-medium"> 30 | {item.title} 31 | </h4> 32 | {item.items ? ( 33 | <NavItems items={item.items} pathname={pathname} /> 34 | ) : null} 35 | </div> 36 | ))} 37 | </aside> 38 | ) 39 | } 40 | 41 | type NavItemsProps = { 42 | items: SidebarNavItem[] 43 | pathname: string | null 44 | } 45 | 46 | function NavItems({ items, pathname }: NavItemsProps) { 47 | return items?.length ? ( 48 | <div className="grid grid-flow-row auto-rows-max text-sm"> 49 | {items.map((item, index) => 50 | !item.disabled && item.href ? ( 51 | <Link 52 | key={index} 53 | href={item.href} 54 | className={cn( 55 | 'flex w-full items-center rounded-md p-2 hover:underline', 56 | { 57 | 'bg-muted': pathname === item.href, 58 | }, 59 | )} 60 | target={item.external ? '_blank' : ''} 61 | rel={item.external ? 'noreferrer' : ''} 62 | > 63 | {item.title} 64 | </Link> 65 | ) : ( 66 | <span 67 | key={index} 68 | className="flex w-full cursor-not-allowed items-center rounded-md p-2 opacity-60" 69 | > 70 | {item.title} 71 | </span> 72 | ), 73 | )} 74 | </div> 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 | <main className="relative py-6 lg:gap-10 lg:py-10 xl:grid xl:grid-cols-[1fr_300px]"> 10 | <div className="mx-auto w-full min-w-0"> 11 | <PageHeader 12 | id="introduction" 13 | className="scroll-m-20" 14 | heading={'Getting started'} 15 | /> 16 | <components.h2>Introduction</components.h2> 17 | <components.p> 18 | <span className="font-bold">useHooks(🔥).ts </span> 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 | <span className="font-semibold">DRY</span> (Don't Repeat 23 | Yourself). There are hooks for most common use cases you might need. 24 | </components.p> 25 | <components.p> 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 | </components.p> 32 | <components.h2>Install</components.h2> 33 | <components.p> 34 | Get started installing{' '} 35 | <components.code> 36 | <components.a href={siteConfig.links.npm}>usehooks-ts</components.a> 37 | </components.code>{' '} 38 | using your preferred package manager. 39 | </components.p> 40 | <CommandCopy 41 | className="my-2" 42 | command={{ 43 | npm: 'npm install usehooks-ts', 44 | pnpm: 'pnpm add usehooks-ts', 45 | yarn: 'yarn add usehooks-ts', 46 | bun: 'bun add usehooks-ts', 47 | }} 48 | /> 49 | </div> 50 | 51 | <RightSidebar 52 | toc={{ 53 | items: [ 54 | { title: 'Introduction', url: '#introduction' }, 55 | { title: 'Install', url: '#install' }, 56 | ], 57 | }} 58 | /> 59 | </main> 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<SetStateAction<number>> 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<SetStepCallbackType>( 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<T> = { 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<T>( 44 | initialValue: T | (() => T), 45 | delay: number, 46 | options?: UseDebounceValueOptions<T>, 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<T>(unwrappedInitialValue) 52 | const previousValueRef = useRef<T | undefined>(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<boolean>(() => { 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, string> | 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 | <code 46 | className={cn( 47 | 'rounded border p-2 gap-2 font-mono text-sm flex dark:bg-[#0d1117] bg-white items-center justify-between', 48 | className, 49 | )} 50 | {...props} 51 | > 52 | <div className="px-2"> 53 | {renderedCommand.split(' ').map((arg, i) => ( 54 | <span 55 | key={arg} 56 | className={cn(i === 0 ? 'font-bold' : 'text-muted-foreground')} 57 | > 58 | {arg}{' '} 59 | </span> 60 | ))} 61 | </div> 62 | <DropdownMenu open={typeof command === 'string' ? false : undefined}> 63 | <DropdownMenuTrigger asChild> 64 | <Button 65 | variant="ghost" 66 | size="icon" 67 | onClick={() => { 68 | if (typeof command === 'string') { 69 | handleCopy(renderedCommand) 70 | } 71 | }} 72 | > 73 | {copiedStatus ? ( 74 | <Check size={16} className="text-muted-foreground" /> 75 | ) : ( 76 | <Copy size={16} className="text-muted-foreground" /> 77 | )} 78 | </Button> 79 | </DropdownMenuTrigger> 80 | <DropdownMenuContent> 81 | {command && 82 | Object.entries(command).map(([key, value]) => ( 83 | <DropdownMenuItem 84 | key={key} 85 | onClick={() => { 86 | handleCopy(value) 87 | }} 88 | > 89 | {key} 90 | </DropdownMenuItem> 91 | ))} 92 | </DropdownMenuContent> 93 | </DropdownMenu> 94 | </code> 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<K, V> = Map<K, V> | [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<K, V> = { 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<K, V>) => 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<K, V>['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<K, V> = [ 33 | Omit<Map<K, V>, 'set' | 'clear' | 'delete'>, 34 | UseMapActions<K, V>, 35 | ] 36 | 37 | /** 38 | * Custom hook that manages a key-value [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) state with setter actions. 39 | * @template K - The type of keys in the map. 40 | * @template V - The type of values in the map. 41 | * @param {MapOrEntries<K, V>} [initialState] - The initial state of the map as a Map or an array of key-value pairs (optional). 42 | * @returns {UseMapReturn<K, V>} A tuple containing the map state and actions to interact with the map. 43 | * @public 44 | * @see [Documentation](https://usehooks-ts.com/react-hook/use-map) 45 | * @example 46 | * ```tsx 47 | * const [map, mapActions] = useMap(); 48 | * // Access the `map` state and use `mapActions` to set, remove, or reset entries. 49 | * ``` 50 | */ 51 | export function useMap<K, V>( 52 | initialState: MapOrEntries<K, V> = new Map(), 53 | ): UseMapReturn<K, V> { 54 | const [map, setMap] = useState(new Map(initialState)) 55 | 56 | const actions: UseMapActions<K, V> = { 57 | set: useCallback((key, value) => { 58 | setMap(prev => { 59 | const copy = new Map(prev) 60 | copy.set(key, value) 61 | return copy 62 | }) 63 | }, []), 64 | 65 | setAll: useCallback(entries => { 66 | setMap(() => new Map(entries)) 67 | }, []), 68 | 69 | remove: useCallback(key => { 70 | setMap(prev => { 71 | const copy = new Map(prev) 72 | copy.delete(key) 73 | return copy 74 | }) 75 | }, []), 76 | 77 | reset: useCallback(() => { 78 | setMap(() => new Map()) 79 | }, []), 80 | } 81 | 82 | return [map, actions] 83 | } 84 | --------------------------------------------------------------------------------