Initial commit

This commit is contained in:
2025-03-07 19:22:02 +01:00
commit 4a98255d83
55743 changed files with 5280367 additions and 0 deletions
+70
View File
@@ -0,0 +1,70 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
name: Checkout [main]
with:
fetch-depth: 0
- name: Setup node
uses: actions/setup-node@v3.5.1
with:
node-version: 18.x
- run: npm install
- run: npm run format:check
- run: npm run type-check
- run: npm run build
- name: "Check for unstaged changes"
run: |
git status --porcelain
git diff-index --quiet HEAD -- || exit 1
compat-modern:
strategy:
matrix:
# rc tag should be react 19 release candidate
react: [18, rc]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
name: Checkout [main]
with:
fetch-depth: 0
- name: Setup node
uses: actions/setup-node@v3.5.1
with:
node-version: 18.x
- run: npm install
- run: npm install react@${{matrix.react}} react-dom@${{matrix.react}} --force
- run: npm run test:ci
env:
REACT_VERSION: ${{matrix.react}}
- run: npm run build
compat-legacy:
strategy:
matrix:
react: [16, 17]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
name: Checkout [main]
with:
fetch-depth: 0
- name: Setup node
uses: actions/setup-node@v3.5.1
with:
node-version: 18.x
- run: npm install
- run: npm install react@${{matrix.react}} react-dom@${{matrix.react}} @testing-library/react-hooks --force
- run: npm run test:ci -- --config ./vitest.16.17.config.ts
env:
REACT_VERSION: ${{matrix.react}}
- run: npm run build
+1
View File
@@ -0,0 +1 @@
{}
+11
View File
@@ -0,0 +1,11 @@
{
"github": {
"release": true
},
"git": {
"changelog": "npx auto-changelog --stdout --commit-limit false -u --template https://raw.githubusercontent.com/release-it/release-it/master/templates/changelog-compact.hbs"
},
"hooks": {
"after:bump": "npx auto-changelog -p"
}
}
+75
View File
@@ -0,0 +1,75 @@
### Changelog
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [1.0.4](https://github.com/microsoft/use-disposable/compare/1.0.3...1.0.4)
- fix: error when building with module bundlers such as webpack [`#37`](https://github.com/microsoft/use-disposable/pull/37)
#### [1.0.3](https://github.com/microsoft/use-disposable/compare/1.0.2...1.0.3)
> 25 October 2024
- chore: update pipeline [`#39`](https://github.com/microsoft/use-disposable/pull/39)
- chore: bump dependencies [`#38`](https://github.com/microsoft/use-disposable/pull/38)
- build(deps-dev): bump vite from 5.2.11 to 5.4.6 [`#36`](https://github.com/microsoft/use-disposable/pull/36)
- build(deps-dev): bump webpack from 5.76.1 to 5.94.0 [`#35`](https://github.com/microsoft/use-disposable/pull/35)
- build(deps-dev): bump braces from 3.0.2 to 3.0.3 [`#34`](https://github.com/microsoft/use-disposable/pull/34)
- build(deps-dev): bump ws from 8.11.0 to 8.17.1 [`#33`](https://github.com/microsoft/use-disposable/pull/33)
- fix: should not throw in React 19 [`#32`](https://github.com/microsoft/use-disposable/pull/32)
- build(deps-dev): bump vite from 3.2.8 to 3.2.10 [`#30`](https://github.com/microsoft/use-disposable/pull/30)
- build(deps-dev): bump ip from 1.1.8 to 1.1.9 [`#29`](https://github.com/microsoft/use-disposable/pull/29)
- build(deps-dev): bump vite from 3.2.7 to 3.2.8 [`#27`](https://github.com/microsoft/use-disposable/pull/27)
- build(deps-dev): bump @babel/traverse from 7.20.5 to 7.23.2 [`#25`](https://github.com/microsoft/use-disposable/pull/25)
- build(deps-dev): bump postcss from 8.4.19 to 8.4.31 [`#24`](https://github.com/microsoft/use-disposable/pull/24)
- Release 1.0.3 [`604502d`](https://github.com/microsoft/use-disposable/commit/604502d69680b0574c67d185cbd2254493997426)
- chore: fix pipeline [`bfa49c2`](https://github.com/microsoft/use-disposable/commit/bfa49c21223dec047c21d4978682d793cce119dd)
#### [1.0.2](https://github.com/microsoft/use-disposable/compare/1.0.1...1.0.2)
> 22 August 2023
- Fixed nullref in useIsStrictMode when external deps provide prod-mode react when expecting dev-mode [`#23`](https://github.com/microsoft/use-disposable/pull/23)
- build(deps-dev): bump word-wrap from 1.2.3 to 1.2.4 [`#22`](https://github.com/microsoft/use-disposable/pull/22)
- build(deps): bump tough-cookie from 4.1.2 to 4.1.3 [`#21`](https://github.com/microsoft/use-disposable/pull/21)
- build(deps): bump vite from 3.2.5 to 3.2.7 [`#20`](https://github.com/microsoft/use-disposable/pull/20)
- build(deps): bump vm2 from 3.9.17 to 3.9.18 [`#19`](https://github.com/microsoft/use-disposable/pull/19)
- build(deps): bump vm2 from 3.9.15 to 3.9.17 [`#18`](https://github.com/microsoft/use-disposable/pull/18)
- build(deps): bump vm2 from 3.9.13 to 3.9.15 [`#15`](https://github.com/microsoft/use-disposable/pull/15)
- build(deps): bump webpack from 5.75.0 to 5.76.1 [`#14`](https://github.com/microsoft/use-disposable/pull/14)
- build(deps): bump json5 from 2.2.1 to 2.2.3 [`#11`](https://github.com/microsoft/use-disposable/pull/11)
- build(deps): bump cacheable-request from 10.2.3 to 10.2.7 [`#13`](https://github.com/microsoft/use-disposable/pull/13)
- build(deps): bump http-cache-semantics from 4.1.0 to 4.1.1 [`#12`](https://github.com/microsoft/use-disposable/pull/12)
- Release 1.0.2 [`dad3d86`](https://github.com/microsoft/use-disposable/commit/dad3d866a382eae1dcdffa661685a9048955ed02)
#### [1.0.1](https://github.com/microsoft/use-disposable/compare/1.0.0...1.0.1)
> 21 December 2022
- fix: Compile to es2019 [`#10`](https://github.com/microsoft/use-disposable/pull/10)
- Release 1.0.1 [`9768e29`](https://github.com/microsoft/use-disposable/commit/9768e29c8fa69039bce580b659ccb0b55480f87e)
### [1.0.0](https://github.com/microsoft/use-disposable/compare/0.1.0-alpha.0...1.0.0)
> 19 December 2022
- chore: CI should check if package-lock changed [`#9`](https://github.com/microsoft/use-disposable/pull/9)
- chore: useIsStrictMode should always return false in production [`#8`](https://github.com/microsoft/use-disposable/pull/8)
- chore: add monosize [`#7`](https://github.com/microsoft/use-disposable/pull/7)
- Release 1.0.0 [`a9fb110`](https://github.com/microsoft/use-disposable/commit/a9fb11088047b9ec4cf9b4cb620d21889796f179)
#### 0.1.0-alpha.0
> 9 December 2022
- Update README.md [`#6`](https://github.com/microsoft/use-disposable/pull/6)
- chore: Create release pipeline yml [`#5`](https://github.com/microsoft/use-disposable/pull/5)
- docs: Initialize README with real docs [`#4`](https://github.com/microsoft/use-disposable/pull/4)
- chore: Setup release scripts and changelog with release-it [`#3`](https://github.com/microsoft/use-disposable/pull/3)
- feat: useDisposable implementation [`#2`](https://github.com/microsoft/use-disposable/pull/2)
- feat: Implement `useIsStrictMode` [`#1`](https://github.com/microsoft/use-disposable/pull/1)
- init repo [`02c3853`](https://github.com/microsoft/use-disposable/commit/02c385328f5fb6c34575f40205f3903b5438815d)
- Install jsdom [`0461e89`](https://github.com/microsoft/use-disposable/commit/0461e8977fba3618860c7c21c25b369d1611d793)
- Initial commit [`ab4dbf5`](https://github.com/microsoft/use-disposable/commit/ab4dbf50f66f0437a4eed166c47a1e0ad2da84f6)
+9
View File
@@ -0,0 +1,9 @@
# Microsoft Open Source Code of Conduct
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
Resources:
- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) Microsoft Corporation.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
+116
View File
@@ -0,0 +1,116 @@
# use-disposable
A hook that creates disposable instances during render phase that works with strict mode.
## Problem
With the introduction of [stricter strict mode in React 18](https://github.com/reactwg/react-18/discussions/19), factories
in hooks like `useMemo` and `useState` are called twice but `useEffect` still disposes once. The component will be mounted and unmounted. This makes
it hard to create instances during render time that would be cleaned up by `useEffect`. Let's look at an example
where we try to create a Portal component.
```tsx
import * as React from "react";
import * as ReactDOM from "react-dom";
function Portal({ children }) {
const domNode = React.useMemo(() => {
const newElement = document.createElement("div");
document.body.appendChild(newElement);
console.log("create DOM node");
return newElement;
}, []);
React.useEffect(() => {
console.log("effect");
return () => {
console.log("dispose DOM node");
domNode.remove();
};
}, [domNode]);
console.log("render to portal");
return ReactDOM.createPortal(children, domNode);
}
```
Let's look at the console output:
```
> create DOM node
> render to portal
> create DOM node
> render to portal
> effect
> dispose DOM node
> effect
```
The result: **Nothing is in the portal** 🚨🚨 why?
During the first `render to portal` the changes are not commited to DOM and in the end there is one empty
portal DOM node. Only after the second render are the DOM changes commited and our content rendered to the
**second** portal DOM node. However this triggers a simulated mount/unmount and our portal DOM
node is disposed 🙃
## useDispose
```tsx
import * as React from "react";
import * as ReactDOM from "react-dom";
import { useDisposable } from "use-disposable";
function Portal({ children }) {
const domNode = React.useDisposable(() => {
const newElement = document.createElement("div");
document.body.appendChild(newElement);
console.log("create DOM node");
return [newElement, () => newElement.remove()];
}, []);
console.log("render to portal");
return ReactDOM.createPortal(children, domNode);
}
```
The resulting console output:
```
> create DOM node
> render to portal
> render to portal
```
`useDispose` makes sure that even in strict mode that in both renders only one instance DOM node is created and used.
## useIsStrictMode
This is a hook that uses the React fiber to detect if the `StrictMode` component is in the tree. It powers `useDisposable` and
is also available from this package as a standalong utility.
## Limitations
⚠️ Calling `useDispose` twice in the same component will lead to unpredictable results, make sure this is only called once
per component, we are actively trying to find ways around this limitation.
## Contributing
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
When you submit a pull request, a CLA bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
## Trademarks
This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft
trademarks or logos is subject to and must follow
[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).
Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
Any use of third-party trademarks or logos are subject to those third-party's policies.
+41
View File
@@ -0,0 +1,41 @@
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.8 BLOCK -->
## Security
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below.
## Reporting Security Issues
**Please do not report security vulnerabilities through public GitHub issues.**
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report).
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey).
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc).
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
- Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
- Full paths of source file(s) related to the manifestation of the issue
- The location of the affected source code (tag/branch/commit or direct URL)
- Any special configuration required to reproduce the issue
- Step-by-step instructions to reproduce the issue
- Proof-of-concept or exploit code (if possible)
- Impact of the issue, including how an attacker might exploit the issue
This information will help us triage your report more quickly.
If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs.
## Preferred Languages
We prefer all communications to be in English.
## Policy
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd).
<!-- END MICROSOFT SECURITY.MD BLOCK -->
+25
View File
@@ -0,0 +1,25 @@
# TODO: The maintainer of this repo has not yet edited this file
**REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project?
- **No CSS support:** Fill out this template with information about how to file issues and get help.
- **Yes CSS support:** Fill out an intake form at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). CSS will work with/help you to determine next steps.
- **Not sure?** Fill out an intake as though the answer were "Yes". CSS will help you decide.
_Then remove this first heading from this SUPPORT.MD file before publishing your repo._
# Support
## How to file issues and get help
This project uses GitHub Issues to track bugs and feature requests. Please search the existing
issues before filing new issues to avoid duplicates. For new issues, file your bug or
feature request as a new Issue.
For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE
FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER
CHANNEL. WHERE WILL YOU HELP PEOPLE?**.
## Microsoft Support Policy
Support for this **PROJECT or PRODUCT** is limited to the resources listed above.
+84
View File
@@ -0,0 +1,84 @@
# Release pipeline
# Variable 'prerelease' was defined in the Variables tab
# Variable 'prereleaseTag' was defined in the Variables tab
# Variable 'publishVersion' was defined in the Variables ta
pr: none
trigger: none
variables:
- group: "Github and NPM secrets"
- name: tags
value: production,externalfacing
resources:
repositories:
- repository: 1esPipelines
type: git
name: 1ESPipelineTemplates/1ESPipelineTemplates
ref: refs/tags/release
extends:
template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines
parameters:
pool:
name: Azure-Pipelines-1ESPT-ExDShared
image: windows-latest
os: windows # We need windows because compliance task only run on windows.
stages:
- stage: main
jobs:
- job: Release
pool:
name: "1ES-Host-Ubuntu"
image: "1ES-PT-Ubuntu-20.04"
os: linux
workspace:
clean: all
steps:
# For multiline scripts, we want the whole task to fail if any line of the script fails.
# ADO doesn't have bash configured this way by default. To fix we override the SHELLOPTS built-in variable.
# https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html
# The options below include ADO defaults (braceexpand:hashall:interactive-comments) plus
# errexit:errtrace for better error behavior.
- script: |
echo "##vso[task.setvariable variable=shellopts]braceexpand:hashall:interactive-comments:errexit:errtrace"
displayName: Force exit on error (bash)
- script: |
git checkout --track "origin/${BUILD_SOURCEBRANCH//refs\/heads\/}"
git pull
displayName: Re-attach HEAD
- task: NodeTool@0
inputs:
versionSpec: "18.x"
checkLatest: true
displayName: "Install Node.js"
- script: npm install
displayName: Install dependencies
- script: |
git config user.name "Fluent UI Build"
git config user.email "fluentui-internal@service.microsoft.com"
git remote set-url origin https://$(githubUser):$(githubPAT)@github.com/microsoft/use-disposable.git
displayName: Authenticate git for pushes
- script: |
echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' > .npmrc
displayName: Write .npmrc
- script: |
npm run release -- $(publishVersion) --ci
displayName: Publish (official)
condition: eq(variables.prerelease, false)
env:
NPM_TOKEN: $(npmToken)
- script: |
npm run release -- $(publishVersion) --preRelease $(prereleaseTag) --ci
displayName: Publish (prerelease)
condition: eq(variables.prerelease, true)
env:
NPM_TOKEN: $(npmToken)
+7
View File
@@ -0,0 +1,7 @@
import { useDisposable } from "use-disposable";
console.log(useDisposable);
export default {
name: "useDisposable",
};
+7
View File
@@ -0,0 +1,7 @@
import { useIsStrictMode } from "use-disposable";
console.log(useIsStrictMode);
export default {
name: "useIsStrictMode",
};
+134
View File
File diff suppressed because one or more lines are too long
+27
View File
@@ -0,0 +1,27 @@
/**
* Traverses up the React fiber tree to find the StrictMode component.
* Note: This only detects strict mode from React >= 18
* https://github.com/reactwg/react-18/discussions/19
* @returns If strict mode is being used in the React tree
*/
declare const useIsStrictMode: () => boolean;
/**
* A factory that returns the disposable instance and it's dispose function
*/
type DisposableFactory<TInstance> = () => [TInstance, () => void];
/**
* Creates a disposable instance during **render time** that will
* be created once (based on dependency array) even during strict mode.
* The disposable will be disposed based on the dependency array similar to
* useEffect.
*
* ⚠️ This can only be called **once** per component
* @param factory - factory for disposable and its dispose function
* @param deps - Similar to a React dependency array
* @returns - The disposable instance
*/
declare function useDisposable<TInstance>(factory: DisposableFactory<TInstance>, deps: any[]): TInstance | null;
export { type DisposableFactory, useDisposable, useIsStrictMode };
+27
View File
@@ -0,0 +1,27 @@
/**
* Traverses up the React fiber tree to find the StrictMode component.
* Note: This only detects strict mode from React >= 18
* https://github.com/reactwg/react-18/discussions/19
* @returns If strict mode is being used in the React tree
*/
declare const useIsStrictMode: () => boolean;
/**
* A factory that returns the disposable instance and it's dispose function
*/
type DisposableFactory<TInstance> = () => [TInstance, () => void];
/**
* Creates a disposable instance during **render time** that will
* be created once (based on dependency array) even during strict mode.
* The disposable will be disposed based on the dependency array similar to
* useEffect.
*
* ⚠️ This can only be called **once** per component
* @param factory - factory for disposable and its dispose function
* @param deps - Similar to a React dependency array
* @returns - The disposable instance
*/
declare function useDisposable<TInstance>(factory: DisposableFactory<TInstance>, deps: any[]): TInstance | null;
export { type DisposableFactory, useDisposable, useIsStrictMode };
+96
View File
File diff suppressed because one or more lines are too long
+26
View File
@@ -0,0 +1,26 @@
import path from "path";
import webpackBundler from "monosize-bundler-webpack";
import upstashStorage from "monosize-storage-upstash";
export default {
repository: "https://github.com/microsoft/use-disposable",
storage: upstashStorage({
url: "https://usw1-bursting-skylark-33255.upstash.io",
readonlyToken:
"AoHnASQgYzI2ZTFiN2YtN2VkNi00YWFjLTkyYTQtNDE4YzRlNGE2NGVlX3VGKBPwppebs7tkUrzjh4xy3kj5xcSi_pq35F_n9As=",
}),
bundler: webpackBundler((config) => {
return {
...config,
resolve: {
...config.resolve,
alias: {
"use-disposable": path.resolve(
new URL(".", import.meta.url).pathname,
"./lib/index.js",
),
},
},
};
}),
};
+52
View File
@@ -0,0 +1,52 @@
{
"name": "use-disposable",
"version": "1.0.4",
"license": "MIT",
"description": "A StrictMode safe hook that will create disposable instance during render",
"main": "lib/index.cjs",
"module": "lib/index.js",
"typings": "lib/index.d.ts",
"types": "lib/index.d.ts",
"type": "module",
"sideEffects": false,
"scripts": {
"build": "tsup --format cjs,esm",
"bundle-size:measure": "monosize measure",
"format": "prettier --write .",
"format:check": "prettier --check .",
"prepublishOnly": "npm run build",
"test": "vitest",
"test:ci": "vitest run",
"type-check": "tsc -b tsconfig.json",
"release": "release-it"
},
"repository": {
"type": "git",
"url": "git+https://github.com/microsoft/use-disposable.git"
},
"bugs": {
"url": "https://github.com/microsoft/use-disposable/issues"
},
"homepage": "https://github.com/microsoft/use-disposable#readme",
"peerDependencies": {
"@types/react": ">=16.8.0 <19.0.0",
"@types/react-dom": ">=16.8.0 <19.0.0",
"react": ">=16.8.0 <19.0.0",
"react-dom": ">=16.8.0 <19.0.0"
},
"devDependencies": {
"@testing-library/react": "13.4.0",
"auto-changelog": "2.5.0",
"jsdom": "25.0.1",
"monosize": "0.6.3",
"monosize-bundler-webpack": "0.1.5",
"monosize-storage-upstash": "0.0.21",
"prettier": "3.3.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"release-it": "17.10.0",
"tsup": "8.3.4",
"typescript": "5.6.3",
"vitest": "2.1.3"
}
}
+3
View File
@@ -0,0 +1,3 @@
export { useIsStrictMode } from "./useIsStrictMode";
export { useDisposable } from "./useDisposable";
export type { DisposableFactory } from "./types";
+4
View File
@@ -0,0 +1,4 @@
/**
* A factory that returns the disposable instance and it's dispose function
*/
export type DisposableFactory<TInstance> = () => [TInstance, () => void];
+33
View File
@@ -0,0 +1,33 @@
import { describe, it, vi, expect } from "vitest";
import { renderHook } from "@testing-library/react";
import * as React from "react";
import { useDisposable } from "./useDisposable";
import { useStrictEffect } from "./useStrictEffect";
import { useStrictMemo } from "./useStrictMemo";
vi.mock("./useStrictMemo.ts");
vi.mock("./useStrictEffect.ts");
describe("useDisposable", () => {
describe("in strict mode", () => {
it("should not call strict effect or memo", () => {
const factory = vi.fn().mockReturnValue(["foo", vi.fn()]);
renderHook(() => useDisposable(factory, []), {
wrapper: React.StrictMode,
});
expect(useStrictEffect).not.toHaveBeenCalled();
expect(useStrictMemo).not.toHaveBeenCalled();
});
});
describe("not strict mode", () => {
it("should not call strict effect or memo", () => {
const factory = vi.fn().mockReturnValue(["foo", vi.fn()]);
renderHook(() => useDisposable(factory, []));
expect(useStrictEffect).not.toHaveBeenCalled();
expect(useStrictMemo).not.toHaveBeenCalled();
});
});
});
+101
View File
@@ -0,0 +1,101 @@
import { describe, it, vi, expect, beforeAll, afterAll } from "vitest";
import { renderHook } from "@testing-library/react";
import * as React from "react";
import { useDisposable } from "./useDisposable";
const _maybe_it = process.env.REACT_VERSION === "rc" ? it.skip : it;
describe("useDisposable", () => {
describe("in strict mode", () => {
it("should call factory once during mount", () => {
const factory = vi.fn().mockReturnValue(["foo", vi.fn()]);
renderHook(() => useDisposable(factory, []), {
wrapper: React.StrictMode,
});
expect(factory).toHaveBeenCalledTimes(1);
});
it("should not call dispose on mount", () => {
const dispose = vi.fn();
renderHook(() => useDisposable(() => ["foo", dispose], []), {
wrapper: React.StrictMode,
});
expect(dispose).toHaveBeenCalledTimes(0);
});
_maybe_it("should call dispose on unmount", () => {
const dispose = vi.fn();
const { unmount } = renderHook(
() => useDisposable(() => ["foo", dispose], []),
{
wrapper: React.StrictMode,
},
);
unmount();
expect(dispose).toHaveBeenCalledTimes(1);
});
_maybe_it(
"should call dispose and call factory if dependencies update",
() => {
const dispose = vi.fn();
const factory = vi.fn().mockReturnValue(["foo", dispose]);
let dep = "foo";
const { rerender } = renderHook(() => useDisposable(factory, [dep]), {
wrapper: React.StrictMode,
});
dep = "bar";
rerender();
expect(dispose).toHaveBeenCalledTimes(1);
expect(factory).toHaveBeenCalledTimes(2);
},
);
});
describe("not strict mode", () => {
it("should call factory once during mount", () => {
const factory = vi.fn().mockReturnValue(["foo", vi.fn()]);
renderHook(() => useDisposable(factory, []), {});
expect(factory).toHaveBeenCalledTimes(1);
});
it("should not call dispose on mount", () => {
const dispose = vi.fn();
renderHook(() => useDisposable(() => ["foo", dispose], []), {});
expect(dispose).toHaveBeenCalledTimes(0);
});
it("should call dispose on unmount", () => {
const dispose = vi.fn();
const { unmount } = renderHook(
() => useDisposable(() => ["foo", dispose], []),
{},
);
unmount();
expect(dispose).toHaveBeenCalledTimes(1);
});
it("should call dispose and call factory if dependencies update", () => {
const dispose = vi.fn();
const factory = vi.fn().mockReturnValue(["foo", dispose]);
let dep = "foo";
const { rerender } = renderHook(() => useDisposable(factory, [dep]), {});
dep = "bar";
rerender();
expect(dispose).toHaveBeenCalledTimes(1);
expect(factory).toHaveBeenCalledTimes(2);
});
});
});
+38
View File
@@ -0,0 +1,38 @@
import * as React from "react";
import type { DisposableFactory } from "./types";
import { useIsStrictMode } from "./useIsStrictMode";
import { useStrictEffect } from "./useStrictEffect";
import { useStrictMemo } from "./useStrictMemo";
/**
* Creates a disposable instance during **render time** that will
* be created once (based on dependency array) even during strict mode.
* The disposable will be disposed based on the dependency array similar to
* useEffect.
*
* ⚠️ This can only be called **once** per component
* @param factory - factory for disposable and its dispose function
* @param deps - Similar to a React dependency array
* @returns - The disposable instance
*/
export function useDisposable<TInstance>(
factory: DisposableFactory<TInstance>,
deps: any[],
) {
// In production, strict mode does not require special handling
const isStrictMode =
useIsStrictMode() && process.env.NODE_ENV !== "production";
const useMemo = isStrictMode ? useStrictMemo : React.useMemo;
const useEffect = isStrictMode ? useStrictEffect : React.useEffect;
const [disposable, dispose] = useMemo(() => factory(), deps) ?? [
null,
() => null,
];
useEffect(() => {
return dispose;
}, deps);
return disposable;
}
+35
View File
@@ -0,0 +1,35 @@
import * as React from "react";
import { renderHook } from "@testing-library/react";
import { beforeEach, describe, expect, it } from "vitest";
import { useIsStrictMode } from "./useIsStrictMode";
describe("useIsStrictMode", () => {
beforeEach(() => {
process.env.NODE_ENV = "test";
});
it("should return `true` if wrapped with StrictMode", () => {
const { result } = renderHook(useIsStrictMode, {
wrapper: React.StrictMode,
});
expect(result.current).toBe(true);
});
it("should return `false` if not wrapped with StrictMode", () => {
const { result } = renderHook(useIsStrictMode);
expect(result.current).toBe(false);
});
it("should return `false` always in production mode", () => {
process.env.NODE_ENV = "production";
const { result } = renderHook(useIsStrictMode, {
wrapper: React.StrictMode,
});
expect(result.current).toBe(false);
});
});
+71
View File
@@ -0,0 +1,71 @@
import * as React from "react";
/**
* @returns Current react fiber being rendered
*/
export const getCurrentOwner = () => {
// Note: String concatenation is used to prevent bundlers to complain with multiple versions of React
try {
// React 19
// using react internals
return (React as any)[
"".concat(
"__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE",
)
].A.getOwner();
} catch {}
try {
// React <18
// using react internals
return (React as any)[
"".concat("__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED")
].ReactCurrentOwner.current;
} catch {
if (process.env.NODE_ENV !== "production") {
console.error(
"use-disposable: failed to get current fiber, please report this bug to maintainers",
);
}
}
};
const REACT_STRICT_MODE_TYPE = /*#__PURE__*/ Symbol.for("react.strict_mode");
/**
* Traverses up the React fiber tree to find the StrictMode component.
* Note: This only detects strict mode from React >= 18
* https://github.com/reactwg/react-18/discussions/19
* @returns If strict mode is being used in the React tree
*/
export const useIsStrictMode = (): boolean => {
// This check violates Rules of Hooks, but "process.env.NODE_ENV" does not change in bundle
// or during application lifecycle
if (process.env.NODE_ENV === "production") {
return false;
}
const isStrictMode = React.useRef<boolean | undefined>(undefined);
const reactMajorVersion = React.useMemo(() => {
return Number(React.version.split(".")[0]);
}, [React.version]);
if (isNaN(reactMajorVersion) || reactMajorVersion < 18) {
return false;
}
if (isStrictMode.current === undefined) {
let currentOwner = getCurrentOwner();
while (currentOwner && currentOwner.return) {
currentOwner = currentOwner.return;
if (
currentOwner.type === REACT_STRICT_MODE_TYPE ||
currentOwner.elementType === REACT_STRICT_MODE_TYPE
) {
isStrictMode.current = true;
}
}
}
return !!isStrictMode.current;
};
+42
View File
@@ -0,0 +1,42 @@
import { describe, it, vi, expect } from "vitest";
import { renderHook } from "@testing-library/react";
import * as React from "react";
import { useStrictEffect } from "./useStrictEffect";
describe("useStrictEffect", () => {
it("should only dispose on unmount in strict mode", () => {
const dispose = vi.fn();
const { unmount } = renderHook(
() =>
useStrictEffect(() => {
return dispose;
}, []),
{ wrapper: React.StrictMode },
);
expect(dispose).toHaveBeenCalledTimes(0);
unmount();
expect(dispose).toHaveBeenCalledTimes(1);
});
it("should dispose when dependency array changes", () => {
const dispose = vi.fn();
let dep = "foo";
const { rerender } = renderHook(
() =>
useStrictEffect(() => {
return dispose;
}, [dep]),
{ wrapper: React.StrictMode },
);
expect(dispose).toHaveBeenCalledTimes(0);
dep = "bar";
rerender();
expect(dispose).toHaveBeenCalledTimes(1);
});
});
+23
View File
@@ -0,0 +1,23 @@
import * as React from "react";
import { getCurrentOwner } from "./useIsStrictMode";
// we know strict mode will render useMemo facory twice
// keep a weak set to detect when the second render happens
const effectSet = new WeakSet();
export function useStrictEffect(
effect: () => () => void,
deps: React.DependencyList | undefined,
) {
const currentOwner = getCurrentOwner();
React.useEffect(() => {
if (!effectSet.has(currentOwner)) {
effectSet.add(currentOwner);
effect();
return;
}
const dispose = effect();
return dispose;
}, deps);
}
+28
View File
@@ -0,0 +1,28 @@
import { describe, it, vi, expect } from "vitest";
import { renderHook } from "@testing-library/react";
import * as React from "react";
import { useStrictMemo } from "./useStrictMemo";
describe("useStrictMemo", () => {
it("should not call factory twice on mount in strict mode", () => {
const factory = vi.fn();
renderHook(() => useStrictMemo(factory, []), {
wrapper: React.StrictMode,
});
expect(factory).toHaveBeenCalledTimes(1);
});
it("should call factory if dependencies update", () => {
const factory = vi.fn();
let dep = "foo";
const { rerender } = renderHook(() => useStrictMemo(factory, [dep]), {
wrapper: React.StrictMode,
});
dep = "bar";
rerender();
expect(factory).toHaveBeenCalledTimes(2);
});
});
+21
View File
@@ -0,0 +1,21 @@
import * as React from "react";
import { getCurrentOwner } from "./useIsStrictMode";
// we know strict mode will render useMemo facory twice
// keep a weak set to detect when the second render happens
const memoSet = new WeakSet();
export function useStrictMemo<TMemoized>(
factory: () => any,
deps: React.DependencyList | undefined,
): TMemoized | null {
return React.useMemo(() => {
const currentOwner = getCurrentOwner();
if (!memoSet.has(currentOwner)) {
memoSet.add(currentOwner);
return null;
}
return factory();
}, deps);
}
+27
View File
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"rootDir": ".",
"target": "ES2019",
"module": "esnext",
"moduleResolution": "node",
"lib": ["ES2019", "dom"],
"sourceMap": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"pretty": true,
"typeRoots": ["node_modules/@types"],
"baseUrl": ".",
"jsx": "react"
},
"include": [],
"exclude": ["node_modules"],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"lib": ["ES2020", "dom"],
"declaration": true,
"inlineSources": true,
"outDir": "dist",
"rootDir": "src"
},
"exclude": ["**/*.spec.ts", "**/*.spec.tsx", "**/*.test.ts", "**/*.test.tsx"],
"include": ["./src/**/*.ts", "./src/**/*.tsx"]
}
+17
View File
@@ -0,0 +1,17 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"outDir": "dist",
"types": ["node"]
},
"include": [
"**/*.spec.ts",
"**/*.spec.tsx",
"**/*.test.ts",
"**/*.test.tsx",
"**/*.d.ts",
"./src/testing/**/*.ts",
"./src/testing/**/*.tsx"
]
}
+9
View File
@@ -0,0 +1,9 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts"],
sourcemap: "inline",
dts: true,
tsconfig: "tsconfig.lib.json",
outDir: "lib",
});
+14
View File
@@ -0,0 +1,14 @@
/// <reference types="vitest" />
import { defineConfig } from "vite";
export default defineConfig({
test: {
environment: "jsdom",
include: ["**/*.16.17.test.ts"],
},
resolve: {
alias: {
"@testing-library/react": "@testing-library/react-hooks",
},
},
});
+9
View File
@@ -0,0 +1,9 @@
/// <reference types="vitest" />
import { defineConfig } from "vite";
export default defineConfig({
test: {
environment: "jsdom",
exclude: ["src/**/*.16.17.test.ts", "node_modules/**"],
},
});