├── .all-contributorsrc
├── .changeset
├── README.md
└── config.json
├── .editorconfig
├── .github
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug-report.yml
│ ├── config.yml
│ └── update-docs.yml
├── screenshot.png
└── workflows
│ ├── ci.yml
│ └── update-algolia-index.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── apps
└── www
│ ├── .eslintrc.cjs
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── env.js
│ ├── next-sitemap.config.js
│ ├── next.config.js
│ ├── package.json
│ ├── postcss.config.cjs
│ ├── public
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── apple-touch-icon.png
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon.ico
│ └── site.webmanifest
│ ├── src
│ ├── app
│ │ ├── (docs)
│ │ │ ├── introduction
│ │ │ │ └── page.tsx
│ │ │ ├── layout.tsx
│ │ │ ├── migrate-to-v3
│ │ │ │ └── page.tsx
│ │ │ └── react-hook
│ │ │ │ └── [slug]
│ │ │ │ └── page.tsx
│ │ ├── (marketing)
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ └── prism.css
│ ├── assets
│ │ └── fonts
│ │ │ ├── CalSans-SemiBold.ttf
│ │ │ ├── CalSans-SemiBold.woff
│ │ │ └── CalSans-SemiBold.woff2
│ ├── components
│ │ ├── buy-me-a-coffee.tsx
│ │ ├── carbon-ads
│ │ │ ├── ads.tsx
│ │ │ ├── index.ts
│ │ │ └── use-script.ts
│ │ ├── command-copy.tsx
│ │ ├── doc-search
│ │ │ ├── command-menu.tsx
│ │ │ ├── doc-search.tsx
│ │ │ ├── footer.tsx
│ │ │ ├── hits.tsx
│ │ │ ├── index.ts
│ │ │ ├── input.tsx
│ │ │ ├── modal.context.tsx
│ │ │ ├── open-button.tsx
│ │ │ ├── types.ts
│ │ │ └── use-cmd-k.ts
│ │ ├── docs
│ │ │ ├── left-sidebar.tsx
│ │ │ ├── page-header.tsx
│ │ │ ├── pager.tsx
│ │ │ ├── right-sidebar.tsx
│ │ │ └── table-of-content.tsx
│ │ ├── main-nav.tsx
│ │ ├── mobile-nav.tsx
│ │ └── ui
│ │ │ ├── button.tsx
│ │ │ ├── command.tsx
│ │ │ ├── components.tsx
│ │ │ ├── dialog.tsx
│ │ │ ├── dropdown-menu.tsx
│ │ │ └── icons.tsx
│ ├── config
│ │ ├── docs.ts
│ │ ├── marketing.ts
│ │ └── site.ts
│ ├── lib
│ │ ├── api.ts
│ │ └── utils.ts
│ └── types
│ │ └── index.ts
│ ├── tailwind.config.js
│ └── tsconfig.json
├── package.json
├── packages
├── eslint-config-custom
│ ├── .eslintrc.cjs
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── index.cjs
│ └── package.json
└── usehooks-ts
│ ├── .eslintrc.cjs
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── package.json
│ ├── src
│ ├── index.ts
│ ├── useBoolean
│ │ ├── index.ts
│ │ ├── useBoolean.demo.tsx
│ │ ├── useBoolean.md
│ │ ├── useBoolean.test.ts
│ │ └── useBoolean.ts
│ ├── useClickAnyWhere
│ │ ├── index.ts
│ │ ├── useClickAnyWhere.demo.tsx
│ │ ├── useClickAnyWhere.md
│ │ ├── useClickAnyWhere.test.ts
│ │ └── useClickAnyWhere.ts
│ ├── useCopyToClipboard
│ │ ├── index.ts
│ │ ├── useCopyToClipboard.demo.tsx
│ │ ├── useCopyToClipboard.md
│ │ ├── useCopyToClipboard.test.ts
│ │ └── useCopyToClipboard.ts
│ ├── useCountdown
│ │ ├── index.ts
│ │ ├── useCountdown.demo.tsx
│ │ ├── useCountdown.md
│ │ ├── useCountdown.test.ts
│ │ └── useCountdown.ts
│ ├── useCounter
│ │ ├── index.ts
│ │ ├── useCounter.demo.tsx
│ │ ├── useCounter.md
│ │ ├── useCounter.test.ts
│ │ └── useCounter.ts
│ ├── useDarkMode
│ │ ├── index.ts
│ │ ├── useDarkMode.demo.tsx
│ │ ├── useDarkMode.md
│ │ ├── useDarkMode.test.ts
│ │ └── useDarkMode.ts
│ ├── useDebounceCallback
│ │ ├── index.ts
│ │ ├── useDebounceCallback.demo.tsx
│ │ ├── useDebounceCallback.md
│ │ ├── useDebounceCallback.test.ts
│ │ └── useDebounceCallback.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.test.tsx
│ │ └── useEventCallback.ts
│ ├── useEventListener
│ │ ├── index.ts
│ │ ├── useEventListener.demo.tsx
│ │ ├── useEventListener.md
│ │ ├── useEventListener.test.ts
│ │ └── useEventListener.ts
│ ├── useHover
│ │ ├── index.ts
│ │ ├── useHover.demo.tsx
│ │ ├── useHover.md
│ │ ├── useHover.test.ts
│ │ └── useHover.ts
│ ├── useIntersectionObserver
│ │ ├── index.ts
│ │ ├── useIntersectionObserver.demo.tsx
│ │ ├── useIntersectionObserver.md
│ │ └── useIntersectionObserver.ts
│ ├── useInterval
│ │ ├── index.ts
│ │ ├── useInterval.demo.tsx
│ │ ├── useInterval.md
│ │ ├── useInterval.test.ts
│ │ └── useInterval.ts
│ ├── useIsClient
│ │ ├── index.ts
│ │ ├── useIsClient.demo.tsx
│ │ ├── useIsClient.md
│ │ ├── useIsClient.test.ts
│ │ └── useIsClient.ts
│ ├── useIsMounted
│ │ ├── index.ts
│ │ ├── useIsMounted.demo.tsx
│ │ ├── useIsMounted.md
│ │ ├── useIsMounted.test.ts
│ │ └── useIsMounted.ts
│ ├── useIsomorphicLayoutEffect
│ │ ├── index.ts
│ │ ├── useIsomorphicLayoutEffect.demo.tsx
│ │ ├── useIsomorphicLayoutEffect.md
│ │ └── useIsomorphicLayoutEffect.ts
│ ├── useLocalStorage
│ │ ├── index.ts
│ │ ├── useLocalStorage.demo.tsx
│ │ ├── useLocalStorage.md
│ │ ├── useLocalStorage.test.ts
│ │ └── useLocalStorage.ts
│ ├── useMap
│ │ ├── index.ts
│ │ ├── useMap.demo.tsx
│ │ ├── useMap.md
│ │ ├── useMap.test.ts
│ │ └── useMap.ts
│ ├── useMediaQuery
│ │ ├── index.ts
│ │ ├── useMediaQuery.demo.tsx
│ │ ├── useMediaQuery.md
│ │ └── useMediaQuery.ts
│ ├── useOnClickOutside
│ │ ├── index.ts
│ │ ├── useOnClickOuside.test.ts
│ │ ├── useOnClickOutside.demo.tsx
│ │ ├── useOnClickOutside.md
│ │ └── useOnClickOutside.ts
│ ├── useReadLocalStorage
│ │ ├── index.ts
│ │ ├── useReadLocalStorage.demo.tsx
│ │ ├── useReadLocalStorage.md
│ │ ├── useReadLocalStorage.test.ts
│ │ └── useReadLocalStorage.ts
│ ├── useResizeObserver
│ │ ├── index.ts
│ │ ├── useResizeObserver.demo.tsx
│ │ ├── useResizeObserver.md
│ │ ├── useResizeObserver.test.tsx
│ │ └── useResizeObserver.ts
│ ├── useScreen
│ │ ├── index.ts
│ │ ├── useScreen.demo.tsx
│ │ ├── useScreen.md
│ │ └── useScreen.ts
│ ├── useScript
│ │ ├── index.ts
│ │ ├── useScript.demo.tsx
│ │ ├── useScript.md
│ │ ├── useScript.test.ts
│ │ └── useScript.ts
│ ├── useScrollLock
│ │ ├── index.ts
│ │ ├── useScrollLock.demo.tsx
│ │ ├── useScrollLock.md
│ │ ├── useScrollLock.test.ts
│ │ └── useScrollLock.ts
│ ├── useSessionStorage
│ │ ├── index.ts
│ │ ├── useSessionStorage.demo.tsx
│ │ ├── useSessionStorage.md
│ │ ├── useSessionStorage.test.ts
│ │ └── useSessionStorage.ts
│ ├── useStep
│ │ ├── index.ts
│ │ ├── useStep.demo.tsx
│ │ ├── useStep.md
│ │ ├── useStep.test.ts
│ │ └── useStep.ts
│ ├── useTernaryDarkMode
│ │ ├── index.ts
│ │ ├── useTernaryDarkMode.demo.tsx
│ │ ├── useTernaryDarkMode.md
│ │ ├── useTernaryDarkMode.test.ts
│ │ └── useTernaryDarkMode.ts
│ ├── useTimeout
│ │ ├── index.ts
│ │ ├── useTimeout.demo.tsx
│ │ ├── useTimeout.md
│ │ ├── useTimeout.test.ts
│ │ └── useTimeout.ts
│ ├── useToggle
│ │ ├── index.ts
│ │ ├── useToggle.demo.tsx
│ │ ├── useToggle.md
│ │ ├── useToggle.test.ts
│ │ └── useToggle.ts
│ ├── useUnmount
│ │ ├── index.ts
│ │ ├── useUnmount.demo.tsx
│ │ ├── useUnmount.md
│ │ ├── useUnmount.test.ts
│ │ └── useUnmount.ts
│ └── useWindowSize
│ │ ├── index.ts
│ │ ├── useWindowSize.demo.tsx
│ │ ├── useWindowSize.md
│ │ ├── useWindowSize.test.ts
│ │ └── useWindowSize.ts
│ ├── tests
│ ├── mocks.ts
│ └── setup.ts
│ ├── tsconfig.json
│ ├── tsup.config.ts
│ └── vitest.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── renovate.json
├── scripts
├── env.js
├── generate-doc.js
├── update-algolia-index.js
├── update-testing-issue.js
└── utils
│ ├── data-transform.js
│ ├── generate-doc-files.js
│ ├── get-hooks.js
│ ├── get-markdown-data.js
│ └── update-readme.js
├── turbo.json
├── turbo
└── generators
│ ├── config.cts
│ └── templates
│ ├── hook
│ ├── hook.demo.tsx.hbs
│ ├── hook.mdx.hbs
│ ├── hook.test.ts.hbs
│ ├── hook.ts.hbs
│ └── index.ts.hbs
│ └── index.ts.hbs
└── typedoc.json
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.github/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juliencrn/usehooks-ts/61949134144d3690fe9f521260a16c779a6d3797/.github/screenshot.png
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "semi": false,
4 | "printWidth": 80,
5 | "singleQuote": true,
6 | "tabWidth": 2,
7 | "trailingComma": "all"
8 | }
9 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/apps/www/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/apps/www/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juliencrn/usehooks-ts/61949134144d3690fe9f521260a16c779a6d3797/apps/www/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/apps/www/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juliencrn/usehooks-ts/61949134144d3690fe9f521260a16c779a6d3797/apps/www/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/apps/www/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juliencrn/usehooks-ts/61949134144d3690fe9f521260a16c779a6d3797/apps/www/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/apps/www/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juliencrn/usehooks-ts/61949134144d3690fe9f521260a16c779a6d3797/apps/www/public/favicon-16x16.png
--------------------------------------------------------------------------------
/apps/www/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juliencrn/usehooks-ts/61949134144d3690fe9f521260a16c779a6d3797/apps/www/public/favicon-32x32.png
--------------------------------------------------------------------------------
/apps/www/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juliencrn/usehooks-ts/61949134144d3690fe9f521260a16c779a6d3797/apps/www/public/favicon.ico
--------------------------------------------------------------------------------
/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"}
--------------------------------------------------------------------------------
/apps/www/src/app/(docs)/introduction/page.tsx:
--------------------------------------------------------------------------------
1 | import { CommandCopy } from '@/components/command-copy'
2 | import { PageHeader } from '@/components/docs/page-header'
3 | import { RightSidebar } from '@/components/docs/right-sidebar'
4 | import { components } from '@/components/ui/components'
5 | import { siteConfig } from '@/config/site'
6 |
7 | export default async function IntroductionPage() {
8 | return (
9 |
10 |
11 |
16 |
Introduction
17 |
18 | useHooks(🔥).ts
19 | is a React hooks library, written in Typescript and easy to use. It
20 | provides a set of hooks that enables you to build your React
21 | applications faster. The hooks are built upon the principles of{' '}
22 | DRY (Don't Repeat
23 | Yourself). There are hooks for most common use cases you might need.
24 |
25 |
26 | The library is designed to be as minimal as possible. It is fully
27 | tree-shakable (using the ESM version), meaning that you only import
28 | the hooks you need, and the rest will be removed from your bundle
29 | making the cost of using this library negligible. Most hooks are
30 | extensively tested and are being used in production environments.
31 |
32 |
Install
33 |
34 | Get started installing{' '}
35 |
36 | usehooks-ts
37 | {' '}
38 | using your preferred package manager.
39 |
40 |
49 |
50 |
51 |
59 |
60 | )
61 | }
62 |
--------------------------------------------------------------------------------
/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 |
41 |
42 |
43 |
44 |
45 | {children}
46 |
47 |
48 | >
49 | )
50 | }
51 |
--------------------------------------------------------------------------------
/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 |
36 |
37 | {children}
38 | >
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/apps/www/src/assets/fonts/CalSans-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juliencrn/usehooks-ts/61949134144d3690fe9f521260a16c779a6d3797/apps/www/src/assets/fonts/CalSans-SemiBold.ttf
--------------------------------------------------------------------------------
/apps/www/src/assets/fonts/CalSans-SemiBold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juliencrn/usehooks-ts/61949134144d3690fe9f521260a16c779a6d3797/apps/www/src/assets/fonts/CalSans-SemiBold.woff
--------------------------------------------------------------------------------
/apps/www/src/assets/fonts/CalSans-SemiBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juliencrn/usehooks-ts/61949134144d3690fe9f521260a16c779a6d3797/apps/www/src/assets/fonts/CalSans-SemiBold.woff2
--------------------------------------------------------------------------------
/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
29 | }
30 |
--------------------------------------------------------------------------------
/apps/www/src/components/carbon-ads/index.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | export * from './ads'
4 |
--------------------------------------------------------------------------------
/apps/www/src/components/carbon-ads/use-script.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react'
2 |
3 | // TODO: We can't use usehooks-ts's useScript because it mounts the script in the document.body, maybe provide a way to specify the mount point
4 | export const useScript = (scriptUrl: string, scriptId: string) => {
5 | const ref = useRef(null)
6 |
7 | useEffect(() => {
8 | const existingScript = document.getElementById(scriptId)
9 |
10 | if (!existingScript) {
11 | const script = document.createElement('script')
12 |
13 | script.setAttribute('async', '')
14 | script.setAttribute('type', 'text/javascript')
15 | script.setAttribute('src', scriptUrl)
16 | script.setAttribute('id', scriptId)
17 |
18 | ref.current?.appendChild(script)
19 | }
20 |
21 | return () => {
22 | if (existingScript) {
23 | existingScript.remove()
24 | }
25 | }
26 | }, [scriptUrl, scriptId])
27 |
28 | return ref
29 | }
30 |
--------------------------------------------------------------------------------
/apps/www/src/components/command-copy.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useState } from 'react'
4 |
5 | import { Check, Copy } from 'lucide-react'
6 | import type { ComponentProps } from 'react'
7 | import { useCopyToClipboard } from 'usehooks-ts'
8 |
9 | import { Button } from './ui/button'
10 | import {
11 | DropdownMenu,
12 | DropdownMenuContent,
13 | DropdownMenuItem,
14 | DropdownMenuTrigger,
15 | } from './ui/dropdown-menu'
16 | import { cn } from '@/lib/utils'
17 |
18 | type CommandCopyProps = {
19 | command: Record | string
20 | defaultCommand?: string
21 | } & ComponentProps<'code'>
22 |
23 | export function CommandCopy({
24 | className,
25 | command,
26 | defaultCommand,
27 | ...props
28 | }: CommandCopyProps) {
29 | const [copiedStatus, setCopiedStatus] = useState(false)
30 | const [, copy] = useCopyToClipboard()
31 |
32 | const handleCopy = (text: string) => {
33 | setCopiedStatus(true)
34 | void copy(text)
35 | setTimeout(() => {
36 | setCopiedStatus(false)
37 | }, 2000)
38 | }
39 | const renderedCommand =
40 | typeof command === 'string'
41 | ? command
42 | : command[defaultCommand ?? Object.keys(command)[0]]
43 |
44 | return (
45 |
52 |
53 | {renderedCommand.split(' ').map((arg, i) => (
54 |
58 | {arg}{' '}
59 |
60 | ))}
61 |
62 |
63 |
64 | {
68 | if (typeof command === 'string') {
69 | handleCopy(renderedCommand)
70 | }
71 | }}
72 | >
73 | {copiedStatus ? (
74 |
75 | ) : (
76 |
77 | )}
78 |
79 |
80 |
81 | {command &&
82 | Object.entries(command).map(([key, value]) => (
83 | {
86 | handleCopy(value)
87 | }}
88 | >
89 | {key}
90 |
91 | ))}
92 |
93 |
94 |
95 | )
96 | }
97 |
--------------------------------------------------------------------------------
/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 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | No results found.
32 |
33 |
34 |
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/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 |
18 |
19 |
24 |
25 |
26 |
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/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()
10 |
11 | if (!results?.index) {
12 | return null
13 | }
14 |
15 | return (
16 | <>
17 | {hits.map(hit => (
18 |
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 | {
45 | handleClose()
46 |
47 | const url = makeUrl(hit.objectID)
48 | router.push(url)
49 | }}
50 | >
51 |
57 |
67 |
68 | )
69 | }
70 |
--------------------------------------------------------------------------------
/apps/www/src/components/doc-search/index.ts:
--------------------------------------------------------------------------------
1 | export * from './doc-search'
2 |
--------------------------------------------------------------------------------
/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(null)
11 |
12 | function setQuery(newQuery: string) {
13 | setInputValue(newQuery)
14 | refine(newQuery)
15 | }
16 |
17 | return (
18 |
53 | )
54 | }
55 |
--------------------------------------------------------------------------------
/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>
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(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 |
43 | {props.children}
44 |
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 |
--------------------------------------------------------------------------------
/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,
11 | 'children' | 'onClick' | 'ref'
12 | >
13 |
14 | export const OpenButton = forwardRef(
15 | function OpenButton(props, ref) {
16 | const { handleOpen } = useCommandMenuContext()
17 |
18 | return (
19 |
29 |
30 |
31 | Quick search...
32 |
33 |
34 | ⌘
35 | {' '}
36 | K
37 |
38 |
39 | )
40 | },
41 | )
42 |
--------------------------------------------------------------------------------
/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 = {
8 | objectID: T
9 | name: T
10 | summary?: T
11 | }
12 |
13 | export type Hit = Fields & {
14 | __position: number
15 | _highlightResult: Fields
16 | }
17 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
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 |
49 | {items.map((item, index) =>
50 | !item.disabled && item.href ? (
51 |
63 | {item.title}
64 |
65 | ) : (
66 |
70 | {item.title}
71 |
72 | ),
73 | )}
74 |
75 | ) : null
76 | }
77 |
--------------------------------------------------------------------------------
/apps/www/src/components/docs/page-header.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils'
2 |
3 | type DocsPageHeaderProps = {
4 | heading: string
5 | text?: string
6 | } & React.HTMLAttributes
7 |
8 | export function PageHeader({
9 | heading,
10 | text,
11 | className,
12 | ...props
13 | }: DocsPageHeaderProps) {
14 | return (
15 | <>
16 |
17 |
18 | {heading}
19 |
20 | {text &&
{text}
}
21 |
22 |
23 | >
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/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 |
22 | {prev && (
23 |
27 |
28 | {prev.title}
29 |
30 | )}
31 | {next && (
32 |
36 | {next.title}
37 |
38 |
39 | )}
40 |
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 |
--------------------------------------------------------------------------------
/apps/www/src/components/docs/right-sidebar.tsx:
--------------------------------------------------------------------------------
1 | import { CarbonAds } from '../carbon-ads'
2 | import type { TableOfContents } from './table-of-content'
3 | import { TableOfContent } from './table-of-content'
4 |
5 | type Props = {
6 | toc: TableOfContents
7 | }
8 |
9 | export function RightSidebar({ toc }: Props) {
10 | return (
11 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/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(false)
22 |
23 | return (
24 |
25 |
26 |
27 |
28 | {siteConfig.name}
29 |
30 |
31 | {items?.length ? (
32 |
33 | {items?.map((item, index) => (
34 |
45 | {item.title}
46 |
47 | ))}
48 |
49 | ) : null}
50 | {
53 | setShowMobileMenu(!showMobileMenu)
54 | }}
55 | >
56 | {showMobileMenu ? : }
57 | Menu
58 |
59 | {showMobileMenu && items && (
60 | {children}
61 | )}
62 |
63 | )
64 | }
65 |
--------------------------------------------------------------------------------
/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 |
25 |
26 |
27 |
28 | {siteConfig.name}
29 |
30 |
31 | {items.map((item, index) => (
32 |
40 | {item.title}
41 |
42 | ))}
43 |
44 | {children}
45 |
46 |
47 | )
48 | }
49 |
--------------------------------------------------------------------------------
/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 &
41 | VariantProps
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : 'button'
46 | return (
47 |
52 | )
53 | },
54 | )
55 | Button.displayName = 'Button'
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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({
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "workspace",
3 | "private": true,
4 | "description": "React hook library, ready to use, written in Typescript.",
5 | "author": "Julien CARON ",
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/eslint-config-custom/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/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/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/eslint-config-custom/index.cjs:
--------------------------------------------------------------------------------
1 | const eslintrc = require('./.eslintrc.cjs')
2 |
3 | module.exports = eslintrc
4 |
--------------------------------------------------------------------------------
/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 ",
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/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .turbo
3 | dist
4 |
--------------------------------------------------------------------------------
/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 ",
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/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/useBoolean/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useBoolean'
2 |
--------------------------------------------------------------------------------
/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 |
14 | Value is {value.toString()}
15 |
16 | set true
17 | set false
18 | toggle
19 | custom toggle
20 | >
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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>
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 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useClickAnyWhere/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useClickAnyWhere'
2 |
--------------------------------------------------------------------------------
/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 Click count: {count}
13 | }
14 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/useCopyToClipboard/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useCopyToClipboard'
2 |
--------------------------------------------------------------------------------
/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 | Click to copy:
19 |
20 | A
21 | B
22 | C
23 |
24 | Copied value: {copiedText ?? 'Nothing is copied yet!'}
25 | >
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/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/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/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} A promise that resolves to `true` if the text was copied successfully, or `false` otherwise.
12 | */
13 | type CopyFn = (text: string) => Promise
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(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 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useCountdown/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useCountdown'
2 |
--------------------------------------------------------------------------------
/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(1000)
9 | const [count, { startCountdown, stopCountdown, resetCountdown }] =
10 | useCountdown({
11 | countStart: 60,
12 | intervalMs: intervalValue,
13 | })
14 |
15 | const handleChangeIntervalValue = (event: ChangeEvent) => {
16 | setIntervalValue(Number(event.target.value))
17 | }
18 | return (
19 |
20 |
Count: {count}
21 |
22 |
27 |
start
28 |
stop
29 |
reset
30 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useCounter/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useCounter'
2 |
--------------------------------------------------------------------------------
/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 | Count is {count}
13 | Increment
14 | Decrement
15 | Reset
16 | Multiply by 2
17 | >
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useCounter/useCounter.md:
--------------------------------------------------------------------------------
1 | A simple abstraction to play with a counter, don't repeat yourself.
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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>
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/useDarkMode/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useDarkMode'
2 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useDarkMode/useDarkMode.demo.tsx:
--------------------------------------------------------------------------------
1 | import { useDarkMode } from './useDarkMode'
2 |
3 | export default function Component() {
4 | const { isDarkMode, toggle, enable, disable } = useDarkMode()
5 |
6 | return (
7 |
8 |
Current theme: {isDarkMode ? 'dark' : 'light'}
9 |
Toggle
10 |
Enable
11 |
Disable
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/useDebounceCallback/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useDebounceCallback'
2 |
--------------------------------------------------------------------------------
/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 |
12 |
Debounced value: {value}
13 |
14 |
debounced(event.target.value)}
18 | />
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/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/useDebounceValue/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useDebounceValue'
2 |
--------------------------------------------------------------------------------
/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 |
8 |
Debounced value: {debouncedValue}
9 |
10 |
setValue(event.target.value)}
14 | />
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useDebounceValue/useDebounceValue.ts:
--------------------------------------------------------------------------------
1 | import { useRef, useState } from 'react'
2 |
3 | import type { DebouncedState } from '../useDebounceCallback'
4 | import { useDebounceCallback } from '../useDebounceCallback'
5 |
6 | /**
7 | * Hook options.
8 | * @template T - The type of the value.
9 | */
10 | type UseDebounceValueOptions = {
11 | /**
12 | * Determines whether the function should be invoked on the leading edge of the timeout.
13 | * @default false
14 | */
15 | leading?: boolean
16 | /**
17 | * Determines whether the function should be invoked on the trailing edge of the timeout.
18 | * @default false
19 | */
20 | trailing?: boolean
21 | /**
22 | * The maximum time the specified function is allowed to be delayed before it is invoked.
23 | */
24 | maxWait?: number
25 | /** A function to determine if the value has changed. Defaults to a function that checks if the value is strictly equal to the previous value. */
26 | equalityFn?: (left: T, right: T) => boolean
27 | }
28 |
29 | /**
30 | * Custom hook that returns a debounced version of the provided value, along with a function to update it.
31 | * @template T - The type of the value.
32 | * @param {T | (() => T)} initialValue - The value to be debounced.
33 | * @param {number} delay - The delay in milliseconds before the value is updated (default is 500ms).
34 | * @param {object} [options] - Optional configurations for the debouncing behavior.
35 | * @returns {[T, DebouncedState<(value: T) => void>]} An array containing the debounced value and the function to update it.
36 | * @public
37 | * @see [Documentation](https://usehooks-ts.com/react-hook/use-debounce-value)
38 | * @example
39 | * ```tsx
40 | * const [debouncedValue, updateDebouncedValue] = useDebounceValue(inputValue, 500, { leading: true });
41 | * ```
42 | */
43 | export function useDebounceValue(
44 | initialValue: T | (() => T),
45 | delay: number,
46 | options?: UseDebounceValueOptions,
47 | ): [T, DebouncedState<(value: T) => void>] {
48 | const eq = options?.equalityFn ?? ((left: T, right: T) => left === right)
49 | const unwrappedInitialValue =
50 | initialValue instanceof Function ? initialValue() : initialValue
51 | const [debouncedValue, setDebouncedValue] = useState(unwrappedInitialValue)
52 | const previousValueRef = useRef(unwrappedInitialValue)
53 |
54 | const updateDebouncedValue = useDebounceCallback(
55 | setDebouncedValue,
56 | delay,
57 | options,
58 | )
59 |
60 | // Update the debounced value if the initial value changes
61 | if (!eq(previousValueRef.current as T, unwrappedInitialValue)) {
62 | updateDebouncedValue(unwrappedInitialValue)
63 | previousValueRef.current = unwrappedInitialValue
64 | }
65 |
66 | return [debouncedValue, updateDebouncedValue]
67 | }
68 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useDocumentTitle/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useDocumentTitle'
2 |
--------------------------------------------------------------------------------
/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/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/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/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(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 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useEventCallback/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useEventCallback'
2 |
--------------------------------------------------------------------------------
/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 Click me
10 | }
11 |
--------------------------------------------------------------------------------
/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/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(Click me )
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(Click me )
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], void> = vi.fn()
29 | const fn1Result = renderHook(() => useEventCallback(fn1))
30 |
31 | expectTypeOf(fn1Result.result.current).toEqualTypeOf<
32 | (event: React.MouseEvent) => void
33 | >()
34 |
35 | const fn2 = undefined as
36 | | Mock<[React.MouseEvent], void>
37 | | undefined
38 | const fn2Result = renderHook(() => useEventCallback(fn2))
39 |
40 | expectTypeOf(fn2Result.result.current).toEqualTypeOf<
41 | ((event: React.MouseEvent) => void) | undefined
42 | >()
43 | })
44 |
45 | it('should allow to pass optional callback without errors', () => {
46 | const optionalFn = undefined as
47 | | ((event: React.MouseEvent) => void)
48 | | undefined
49 |
50 | const { result } = renderHook(() => useEventCallback(optionalFn))
51 |
52 | render(Click me )
53 |
54 | fireEvent.click(screen.getByText('Click me'))
55 | })
56 | })
57 |
--------------------------------------------------------------------------------
/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(
21 | fn: (...args: Args) => R,
22 | ): (...args: Args) => R
23 | export function useEventCallback(
24 | fn: ((...args: Args) => R) | undefined,
25 | ): ((...args: Args) => R) | undefined
26 | export function useEventCallback(
27 | fn: ((...args: Args) => R) | undefined,
28 | ): ((...args: Args) => R) | undefined {
29 | const ref = useRef(() => {
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 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useEventListener/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useEventListener'
2 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useEventListener/useEventListener.demo.tsx:
--------------------------------------------------------------------------------
1 | import { useRef } from 'react'
2 |
3 | import { useEventListener } from './useEventListener'
4 |
5 | export default function Component() {
6 | // Define button ref
7 | const buttonRef = useRef(null)
8 | const documentRef = useRef(document)
9 |
10 | const onScroll = (event: Event) => {
11 | console.log('window scrolled!', event)
12 | }
13 |
14 | const onClick = (event: Event) => {
15 | console.log('button clicked!', event)
16 | }
17 |
18 | const onVisibilityChange = (event: Event) => {
19 | console.log('doc visibility changed!', {
20 | isVisible: !document.hidden,
21 | event,
22 | })
23 | }
24 |
25 | // example with window based event
26 | useEventListener('scroll', onScroll)
27 |
28 | // example with document based event
29 | useEventListener('visibilitychange', onVisibilityChange, documentRef)
30 |
31 | // example with element based event
32 | useEventListener('click', onClick, buttonRef)
33 |
34 | return (
35 |
36 | Click me
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/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/useHover/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useHover'
2 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useHover/useHover.demo.tsx:
--------------------------------------------------------------------------------
1 | import { useRef } from 'react'
2 |
3 | import { useHover } from './useHover'
4 |
5 | export default function Component() {
6 | const hoverRef = useRef(null)
7 | const isHover = useHover(hoverRef)
8 |
9 | return (
10 |
11 | {`The current div is ${isHover ? `hovered` : `unhovered`}`}
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/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/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 |
--------------------------------------------------------------------------------
/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} 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(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(
22 | elementRef: RefObject,
23 | ): boolean {
24 | const [value, setValue] = useState(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/useIntersectionObserver/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useIntersectionObserver'
2 |
--------------------------------------------------------------------------------
/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 |
24 | )
25 | }
26 |
27 | export default function Component() {
28 | return (
29 | <>
30 | {Array.from({ length: 5 }).map((_, index) => (
31 |
32 | ))}
33 | >
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/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/useInterval/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useInterval'
2 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useInterval/useInterval.demo.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 |
3 | import type { ChangeEvent } from 'react'
4 |
5 | import { useInterval } from './useInterval'
6 |
7 | export default function Component() {
8 | // The counter
9 | const [count, setCount] = useState(0)
10 | // Dynamic delay
11 | const [delay, setDelay] = useState(1000)
12 | // ON/OFF
13 | const [isPlaying, setPlaying] = useState(false)
14 |
15 | useInterval(
16 | () => {
17 | // Your custom logic here
18 | setCount(count + 1)
19 | },
20 | // Delay in milliseconds or null to stop it
21 | isPlaying ? delay : null,
22 | )
23 |
24 | const handleChange = (event: ChangeEvent) => {
25 | setDelay(Number(event.target.value))
26 | }
27 |
28 | return (
29 | <>
30 | {count}
31 | {
33 | setPlaying(!isPlaying)
34 | }}
35 | >
36 | {isPlaying ? 'pause' : 'play'}
37 |
38 |
39 | Delay:
40 |
46 |
47 | >
48 | )
49 | }
50 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/useIsClient/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useIsClient'
2 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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/useIsMounted/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useIsMounted'
2 |
--------------------------------------------------------------------------------
/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 {data}
19 | }
20 |
21 | export default function Component() {
22 | const [isVisible, setVisible] = useState(false)
23 |
24 | const toggleVisibility = () => {
25 | setVisible(state => !state)
26 | }
27 |
28 | return (
29 | <>
30 | {isVisible ? 'Hide' : 'Show'}
31 |
32 | {isVisible && }
33 | >
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/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/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/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/useIsomorphicLayoutEffect/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useIsomorphicLayoutEffect'
2 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.demo.tsx:
--------------------------------------------------------------------------------
1 | import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'
2 |
3 | export default function Component() {
4 | useIsomorphicLayoutEffect(() => {
5 | console.log(
6 | "In the browser, I'm an `useLayoutEffect`, but in SSR, I'm an `useEffect`.",
7 | )
8 | }, [])
9 |
10 | return Hello, world
11 | }
12 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/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 |
--------------------------------------------------------------------------------
/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} [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 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useLocalStorage/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useLocalStorage'
2 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useLocalStorage/useLocalStorage.demo.tsx:
--------------------------------------------------------------------------------
1 | import { useLocalStorage } from './useLocalStorage'
2 |
3 | export default function Component() {
4 | const [value, setValue, removeValue] = useLocalStorage('test-key', 0)
5 |
6 | return (
7 |
8 |
Count: {value}
9 |
{
11 | setValue((x: number) => x + 1)
12 | }}
13 | >
14 | Increment
15 |
16 |
{
18 | setValue((x: number) => x - 1)
19 | }}
20 | >
21 | Decrement
22 |
23 |
{
25 | removeValue()
26 | }}
27 | >
28 | Reset
29 |
30 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/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/useMap/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useMap'
2 |
--------------------------------------------------------------------------------
/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([['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 |
26 |
Add
27 |
Reset
28 |
Set new data
29 |
30 | {'Remove "hello"'}
31 |
32 |
33 |
34 | Map (
35 | {Array.from(map.entries()).map(([key, value]) => (
36 | {`\n ${key}: ${value}`}
37 | ))}
38 | )
39 |
40 |
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useMap/useMap.md:
--------------------------------------------------------------------------------
1 | This React hook provides an API to interact with a `Map` ([Documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map))
2 |
3 | It takes as initial entries a `Map` or an array like `[["key": "value"], [..]]` or nothing and returns:
4 |
5 | - An array with an instance of `Map` (including: `foreach, get, has, entries, keys, values, size`)
6 | - And an object of methods (`set, setAll, remove, reset`)
7 |
8 | Make sure to use these methods to update the map, a `map.set(..)` would not re-render the component.
9 |
10 |
11 |
12 | **Why use Map instead of an object ?**
13 |
14 | Map is an iterable, a simple hash and it performs better in storing large data ([Read more](https://azimi.io/es6-map-with-react-usestate-9175cd7b409b)).
15 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useMap/useMap.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'react'
2 |
3 | /**
4 | * Represents the type for either a Map or an array of key-value pairs.
5 | * @template K - The type of keys in the map.
6 | * @template V - The type of values in the map.
7 | */
8 | type MapOrEntries = Map | [K, V][]
9 |
10 | /**
11 | * Represents the actions available to interact with the map state.
12 | * @template K - The type of keys in the map.
13 | * @template V - The type of values in the map.
14 | */
15 | type UseMapActions = {
16 | /** Set a key-value pair in the map. */
17 | set: (key: K, value: V) => void
18 | /** Set all key-value pairs in the map. */
19 | setAll: (entries: MapOrEntries) => void
20 | /** Remove a key-value pair from the map. */
21 | remove: (key: K) => void
22 | /** Reset the map to an empty state. */
23 | reset: Map['clear']
24 | }
25 |
26 | /**
27 | * Represents the return type of the `useMap` hook.
28 | * We hide some setters from the returned map to disable autocompletion.
29 | * @template K - The type of keys in the map.
30 | * @template V - The type of values in the map.
31 | */
32 | type UseMapReturn = [
33 | Omit, 'set' | 'clear' | 'delete'>,
34 | UseMapActions,
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} [initialState] - The initial state of the map as a Map or an array of key-value pairs (optional).
42 | * @returns {UseMapReturn} 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(
52 | initialState: MapOrEntries = new Map(),
53 | ): UseMapReturn {
54 | const [map, setMap] = useState(new Map(initialState))
55 |
56 | const actions: UseMapActions = {
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 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useMediaQuery/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useMediaQuery'
2 |
--------------------------------------------------------------------------------
/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 |
8 | {`The view port is ${matches ? 'at least' : 'less than'} 768 pixels wide`}
9 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/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/useMediaQuery/useMediaQuery.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 |
3 | import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect'
4 |
5 | /** Hook options. */
6 | type UseMediaQueryOptions = {
7 | /**
8 | * The default value to return if the hook is being run on the server.
9 | * @default false
10 | */
11 | defaultValue?: boolean
12 | /**
13 | * If `true` (default), the hook will initialize reading the media query. In SSR, you should set it to `false`, returning `options.defaultValue` or `false` initially.
14 | * @default true
15 | */
16 | initializeWithValue?: boolean
17 | }
18 |
19 | const IS_SERVER = typeof window === 'undefined'
20 |
21 | /**
22 | * Custom hook that tracks the state of a media query using the [`Match Media API`](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia).
23 | * @param {string} query - The media query to track.
24 | * @param {?UseMediaQueryOptions} [options] - The options for customizing the behavior of the hook (optional).
25 | * @returns {boolean} The current state of the media query (true if the query matches, false otherwise).
26 | * @public
27 | * @see [Documentation](https://usehooks-ts.com/react-hook/use-media-query)
28 | * @example
29 | * ```tsx
30 | * const isSmallScreen = useMediaQuery('(max-width: 600px)');
31 | * // Use `isSmallScreen` to conditionally apply styles or logic based on the screen size.
32 | * ```
33 | */
34 | export function useMediaQuery(
35 | query: string,
36 | {
37 | defaultValue = false,
38 | initializeWithValue = true,
39 | }: UseMediaQueryOptions = {},
40 | ): boolean {
41 | const getMatches = (query: string): boolean => {
42 | if (IS_SERVER) {
43 | return defaultValue
44 | }
45 | return window.matchMedia(query).matches
46 | }
47 |
48 | const [matches, setMatches] = useState(() => {
49 | if (initializeWithValue) {
50 | return getMatches(query)
51 | }
52 | return defaultValue
53 | })
54 |
55 | // Handles the change event of the media query.
56 | function handleChange() {
57 | setMatches(getMatches(query))
58 | }
59 |
60 | useIsomorphicLayoutEffect(() => {
61 | const matchMedia = window.matchMedia(query)
62 |
63 | // Triggered at the first client-side load and if query changes
64 | handleChange()
65 |
66 | // Use deprecated `addListener` and `removeListener` to support Safari < 14 (#135)
67 | if (matchMedia.addListener) {
68 | matchMedia.addListener(handleChange)
69 | } else {
70 | matchMedia.addEventListener('change', handleChange)
71 | }
72 |
73 | return () => {
74 | if (matchMedia.removeListener) {
75 | matchMedia.removeListener(handleChange)
76 | } else {
77 | matchMedia.removeEventListener('change', handleChange)
78 | }
79 | }
80 | }, [query])
81 |
82 | return matches
83 | }
84 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useOnClickOutside/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useOnClickOutside'
2 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useOnClickOutside/useOnClickOutside.demo.tsx:
--------------------------------------------------------------------------------
1 | import { useRef } from 'react'
2 |
3 | import { useOnClickOutside } from './useOnClickOutside'
4 |
5 | export default function Component() {
6 | const ref = useRef(null)
7 |
8 | const handleClickOutside = () => {
9 | // Your custom logic here
10 | console.log('clicked outside')
11 | }
12 |
13 | const handleClickInside = () => {
14 | // Your custom logic here
15 | console.log('clicked inside')
16 | }
17 |
18 | useOnClickOutside(ref, handleClickOutside)
19 |
20 | return (
21 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/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/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 | RefObject[]} 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(
33 | ref: RefObject | RefObject[],
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/useReadLocalStorage/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useReadLocalStorage'
2 |
--------------------------------------------------------------------------------
/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 DarkMode is {darkMode ? 'enabled' : 'disabled'}
8 | }
9 |
--------------------------------------------------------------------------------
/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/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/useResizeObserver/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useResizeObserver'
2 |
--------------------------------------------------------------------------------
/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(null)
13 | const { width = 0, height = 0 } = useResizeObserver({
14 | ref,
15 | box: 'border-box',
16 | })
17 |
18 | return (
19 |
20 | {width} x {height}
21 |
22 | )
23 | }
24 |
25 | export function WithDebounce() {
26 | const ref = useRef(null)
27 | const [{ width, height }, setSize] = useState({
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 |
50 | debounced: {width} x {height}
51 |
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useScreen/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useScreen'
2 |
--------------------------------------------------------------------------------
/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 |
8 | The current window dimensions are:{' '}
9 | {JSON.stringify(screen, null, 2)}
10 |
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useScript/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useScript'
2 |
--------------------------------------------------------------------------------
/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 |
26 |
{`Current status: ${status}`}
27 |
28 | {status === 'ready' &&
You can use the script here.
}
29 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/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 ` ` on all your pages if it is not necessary.
4 |
--------------------------------------------------------------------------------
/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/useScrollLock/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useScrollLock'
2 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useScrollLock/useScrollLock.demo.tsx:
--------------------------------------------------------------------------------
1 | import { useScrollLock } from './useScrollLock'
2 |
3 | // Example 1: Auto lock the scroll of the body element when the modal mounts
4 | export default function Modal() {
5 | useScrollLock()
6 | return Modal
7 | }
8 |
9 | // Example 2: Manually lock and unlock the scroll for a specific target
10 | export function App() {
11 | const { lock, unlock } = useScrollLock({
12 | autoLock: false,
13 | lockTarget: '#scrollable',
14 | })
15 |
16 | return (
17 | <>
18 |
23 |
24 |
25 | Lock
26 | Unlock
27 |
28 | >
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/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/useSessionStorage/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useSessionStorage'
2 |
--------------------------------------------------------------------------------
/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 |
8 |
Count: {value}
9 |
{
11 | setValue((x: number) => x + 1)
12 | }}
13 | >
14 | Increment
15 |
16 |
{
18 | setValue((x: number) => x - 1)
19 | }}
20 | >
21 | Decrement
22 |
23 |
{
25 | removeValue()
26 | }}
27 | >
28 | Reset
29 |
30 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/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/useStep/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useStep'
2 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useStep/useStep.demo.tsx:
--------------------------------------------------------------------------------
1 | import { useStep } from './useStep'
2 |
3 | export default function Component() {
4 | const [currentStep, helpers] = useStep(5)
5 |
6 | const {
7 | canGoToPrevStep,
8 | canGoToNextStep,
9 | goToNextStep,
10 | goToPrevStep,
11 | reset,
12 | setStep,
13 | } = helpers
14 |
15 | return (
16 | <>
17 | Current step is {currentStep}
18 | Can go to previous step {canGoToPrevStep ? 'yes' : 'no'}
19 | Can go to next step {canGoToNextStep ? 'yes' : 'no'}
20 | Go to next step
21 | Go to previous step
22 | Reset
23 | {
25 | setStep(3)
26 | }}
27 | >
28 | Set to step 3
29 |
30 | >
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useStep/useStep.md:
--------------------------------------------------------------------------------
1 | A simple abstraction to play with a stepper, don't repeat yourself.
2 |
--------------------------------------------------------------------------------
/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/useStep/useStep.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'react'
2 |
3 | import type { Dispatch, SetStateAction } from 'react'
4 |
5 | /** Represents the second element of the output of the `useStep` hook. */
6 | type UseStepActions = {
7 | /** Go to the next step in the process. */
8 | goToNextStep: () => void
9 | /** Go to the previous step in the process. */
10 | goToPrevStep: () => void
11 | /** Reset the step to the initial step. */
12 | reset: () => void
13 | /** Check if the next step is available. */
14 | canGoToNextStep: boolean
15 | /** Check if the previous step is available. */
16 | canGoToPrevStep: boolean
17 | /** Set the current step to a specific value. */
18 | setStep: Dispatch>
19 | }
20 |
21 | type SetStepCallbackType = (step: number | ((step: number) => number)) => void
22 |
23 | /**
24 | * Custom hook that manages and navigates between steps in a multi-step process.
25 | * @param {number} maxStep - The maximum step in the process.
26 | * @returns {[number, UseStepActions]} An tuple containing the current step and helper functions for navigating steps.
27 | * @public
28 | * @see [Documentation](https://usehooks-ts.com/react-hook/use-step)
29 | * @example
30 | * ```tsx
31 | * const [currentStep, { goToNextStep, goToPrevStep, reset, canGoToNextStep, canGoToPrevStep, setStep }] = useStep(3);
32 | * // Access and use the current step and provided helper functions.
33 | * ```
34 | */
35 | export function useStep(maxStep: number): [number, UseStepActions] {
36 | const [currentStep, setCurrentStep] = useState(1)
37 |
38 | const canGoToNextStep = currentStep + 1 <= maxStep
39 | const canGoToPrevStep = currentStep - 1 > 0
40 |
41 | const setStep = useCallback(
42 | step => {
43 | // Allow value to be a function so we have the same API as useState
44 | const newStep = step instanceof Function ? step(currentStep) : step
45 |
46 | if (newStep >= 1 && newStep <= maxStep) {
47 | setCurrentStep(newStep)
48 | return
49 | }
50 |
51 | throw new Error('Step not valid')
52 | },
53 | [maxStep, currentStep],
54 | )
55 |
56 | const goToNextStep = useCallback(() => {
57 | if (canGoToNextStep) {
58 | setCurrentStep(step => step + 1)
59 | }
60 | }, [canGoToNextStep])
61 |
62 | const goToPrevStep = useCallback(() => {
63 | if (canGoToPrevStep) {
64 | setCurrentStep(step => step - 1)
65 | }
66 | }, [canGoToPrevStep])
67 |
68 | const reset = useCallback(() => {
69 | setCurrentStep(1)
70 | }, [])
71 |
72 | return [
73 | currentStep,
74 | {
75 | goToNextStep,
76 | goToPrevStep,
77 | canGoToNextStep,
78 | canGoToPrevStep,
79 | setStep,
80 | reset,
81 | },
82 | ]
83 | }
84 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useTernaryDarkMode/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useTernaryDarkMode'
2 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useTernaryDarkMode/useTernaryDarkMode.demo.tsx:
--------------------------------------------------------------------------------
1 | import { useTernaryDarkMode } from './useTernaryDarkMode'
2 |
3 | type TernaryDarkMode = ReturnType['ternaryDarkMode']
4 |
5 | export default function Component() {
6 | const {
7 | isDarkMode,
8 | ternaryDarkMode,
9 | setTernaryDarkMode,
10 | toggleTernaryDarkMode,
11 | } = useTernaryDarkMode()
12 |
13 | return (
14 |
15 |
Current theme: {isDarkMode ? 'dark' : 'light'}
16 |
ternaryMode: {ternaryDarkMode}
17 |
18 | Toggle between three modes
19 |
20 | Toggle from {ternaryDarkMode}
21 |
22 |
23 |
24 | Select a mode
25 |
26 | {
29 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
30 | setTernaryDarkMode(ev.target.value as TernaryDarkMode)
31 | }}
32 | value={ternaryDarkMode}
33 | >
34 | light
35 | system
36 | dark
37 |
38 |
39 |
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useTimeout/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useTimeout'
2 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useTimeout/useTimeout.demo.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 |
3 | import { useTimeout } from './useTimeout'
4 |
5 | export default function Component() {
6 | const [visible, setVisible] = useState(true)
7 |
8 | const hide = () => {
9 | setVisible(false)
10 | }
11 |
12 | useTimeout(hide, 5000)
13 |
14 | return (
15 |
16 |
17 | {visible
18 | ? "I'm visible for 5000ms"
19 | : 'You can no longer see this content'}
20 |
21 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useToggle/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useToggle'
2 |
--------------------------------------------------------------------------------
/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 |
14 | Value is {value.toString()}
15 |
16 | {
18 | setValue(true)
19 | }}
20 | >
21 | set true
22 |
23 | {
25 | setValue(false)
26 | }}
27 | >
28 | set false
29 |
30 | toggle
31 | custom toggle
32 | >
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useToggle/useToggle.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'react'
2 |
3 | import type { Dispatch, SetStateAction } from 'react'
4 |
5 | /**
6 | * Custom hook that manages a boolean toggle state in React components.
7 | * @param {boolean} [defaultValue] - The initial value for the toggle state.
8 | * @returns {[boolean, () => void, Dispatch>]} A tuple containing the current state,
9 | * a function to toggle the state, and a function to set the state explicitly.
10 | * @public
11 | * @see [Documentation](https://usehooks-ts.com/react-hook/use-toggle)
12 | * @example
13 | * ```tsx
14 | * const [isToggled, toggle, setToggle] = useToggle(); // Initial value is false
15 | * // OR
16 | * const [isToggled, toggle, setToggle] = useToggle(true); // Initial value is true
17 | * // Use isToggled in your component, toggle to switch the state, setToggle to set the state explicitly.
18 | * ```
19 | */
20 | export function useToggle(
21 | defaultValue?: boolean,
22 | ): [boolean, () => void, Dispatch>] {
23 | const [value, setValue] = useState(!!defaultValue)
24 |
25 | const toggle = useCallback(() => {
26 | setValue(x => !x)
27 | }, [])
28 |
29 | return [value, toggle, setValue]
30 | }
31 |
--------------------------------------------------------------------------------
/packages/usehooks-ts/src/useUnmount/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useUnmount'
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/useWindowSize/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useWindowSize'
2 |
--------------------------------------------------------------------------------
/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 |
8 | The current window dimensions are:{' '}
9 | {JSON.stringify({ width, height })}
10 |
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 {
30 | store: Record = {}
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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - 'apps/*'
3 | - 'packages/*'
4 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | /(.*)/gms,
11 | `\n\n${hooks.map(formatHook).join('\n')}\n`,
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/turbo/generators/templates/hook/hook.mdx.hbs:
--------------------------------------------------------------------------------
1 | This hook description markdown text.
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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>
11 | }
12 |
13 | /**
14 | * Custom hook that ...
15 | * @param {boolean} [defaultValue] - The initial value for ... (default is `0`).
16 | * @returns {[number, Dispatch>]} 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 |
--------------------------------------------------------------------------------
/turbo/generators/templates/hook/index.ts.hbs:
--------------------------------------------------------------------------------
1 | export * from './{{camelCase name}}'
2 |
--------------------------------------------------------------------------------
/turbo/generators/templates/index.ts.hbs:
--------------------------------------------------------------------------------
1 | export * from './{{camelCase name}}'
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------