├── .babelrc ├── .coveralls.yml ├── .cursor └── rules │ ├── demo.mdc │ ├── docs.mdc │ ├── git.mdc │ ├── project.mdc │ ├── testing.mdc │ └── typescript.mdc ├── .editorconfig ├── .eslintrc.js ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE │ └── pr_cn.md └── workflows │ ├── comment-when-needs-more-info.yml │ ├── gitleaks.yml │ ├── issue-close-require.yml │ ├── issue-reply.yml │ ├── static.yml │ └── test.yml ├── .gitignore ├── .gitleaks.toml ├── .husky ├── commit-msg └── pre-commit ├── .npmrc ├── .travis.yml ├── CONTRIBUTING.MD ├── CONTRIBUTING.zh-CN.MD ├── LICENSE ├── README.md ├── README.zh-CN.md ├── SECURITY.md ├── biome.json ├── config ├── config.ts └── hooks.ts ├── dingtalk.jpg ├── docs ├── guide │ ├── blog │ │ ├── function.en-US.md │ │ ├── function.zh-CN.md │ │ ├── hmr.en-US.md │ │ ├── hmr.zh-CN.md │ │ ├── ssr.en-US.md │ │ ├── ssr.zh-CN.md │ │ ├── strict.en-US.md │ │ └── strict.zh-CN.md │ ├── dom.en-US.md │ ├── dom.zh-CN.md │ ├── index.en-US.md │ ├── index.zh-CN.md │ ├── upgrade.en-US.md │ └── upgrade.zh-CN.md ├── index.en-US.md └── index.zh-CN.md ├── example └── .gitkeep ├── gulpfile.js ├── jest.config.js ├── jest.setup.js ├── match-media-mock.js ├── package.json ├── packages ├── hooks │ ├── gulpfile.js │ ├── package.json │ ├── src │ │ ├── __tests__ │ │ │ └── index.test.ts │ │ ├── createDeepCompareEffect │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ └── index.ts │ │ ├── createUpdateEffect │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ └── index.ts │ │ ├── createUseStorageState │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ └── index.ts │ │ ├── global.d.ts │ │ ├── index.ts │ │ ├── useAntdTable │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ ├── cache.tsx │ │ │ │ ├── form.tsx │ │ │ │ ├── init.tsx │ │ │ │ ├── ready.tsx │ │ │ │ ├── table.tsx │ │ │ │ └── validate.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.tsx │ │ │ ├── index.zh-CN.md │ │ │ └── types.ts │ │ ├── useAsyncEffect │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ └── demo2.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useBoolean │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useClickAway │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ ├── demo2.tsx │ │ │ │ ├── demo3.tsx │ │ │ │ ├── demo4.tsx │ │ │ │ ├── demo5.tsx │ │ │ │ └── demo6.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useControllableValue │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ ├── demo2.tsx │ │ │ │ └── demo3.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useCookieState │ │ │ ├── __tests__ │ │ │ │ └── index.test.tsx │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ ├── demo2.tsx │ │ │ │ └── demo3.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useCountDown │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ ├── demo2.tsx │ │ │ │ └── demo3.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useCounter │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useCreation │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useDebounce │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── debounceOptions.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useDebounceEffect │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useDebounceFn │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useDeepCompareEffect │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.tsx │ │ │ └── index.zh-CN.md │ │ ├── useDeepCompareLayoutEffect │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.tsx │ │ │ └── index.zh-CN.md │ │ ├── useDocumentVisibility │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useDrag │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ └── index.ts │ │ ├── useDrop │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ └── demo2.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useDynamicList │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ ├── demo2.tsx │ │ │ │ ├── demo3.tsx │ │ │ │ └── demo4.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useEventEmitter │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useEventListener │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ ├── demo2.tsx │ │ │ │ └── demo3.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useEventTarget │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ └── demo2.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useExternal │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ ├── demo2.tsx │ │ │ │ └── demo3.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useFavicon │ │ │ ├── __tests__ │ │ │ │ └── index.test.tsx │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useFocusWithin │ │ │ ├── __tests__ │ │ │ │ └── index.test.tsx │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ └── demo2.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.tsx │ │ │ └── index.zh-CN.md │ │ ├── useFullscreen │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ ├── demo2.tsx │ │ │ │ ├── demo3.tsx │ │ │ │ ├── demo4.tsx │ │ │ │ └── react-hooks.jpg │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useFusionTable │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ ├── cache.tsx │ │ │ │ ├── form.tsx │ │ │ │ ├── init.tsx │ │ │ │ ├── table.tsx │ │ │ │ └── validate.tsx │ │ │ ├── fusionAdapter.ts │ │ │ ├── index.en-US.md │ │ │ ├── index.tsx │ │ │ ├── index.zh-CN.md │ │ │ └── types.ts │ │ ├── useGetState │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useHistoryTravel │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ ├── demo2.tsx │ │ │ │ └── demo3.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useHover │ │ │ ├── __tests__ │ │ │ │ └── index.test.tsx │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ └── demo2.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useInViewport │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ ├── demo2.tsx │ │ │ │ └── demo3.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useInfiniteScroll │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ ├── default.tsx │ │ │ │ ├── mutate.tsx │ │ │ │ ├── pagination.tsx │ │ │ │ ├── reload.tsx │ │ │ │ ├── scroll.tsx │ │ │ │ └── scrollTop.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.tsx │ │ │ ├── index.zh-CN.md │ │ │ └── types.ts │ │ ├── useInterval │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ └── demo2.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useIsomorphicLayoutEffect │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useKeyPress │ │ │ ├── __tests__ │ │ │ │ └── index.test.tsx │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ ├── demo2.tsx │ │ │ │ ├── demo3.tsx │ │ │ │ ├── demo4.tsx │ │ │ │ ├── demo5.tsx │ │ │ │ ├── demo6.tsx │ │ │ │ ├── demo7.tsx │ │ │ │ └── demo8.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useLatest │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useLocalStorageState │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ ├── demo2.tsx │ │ │ │ ├── demo3.tsx │ │ │ │ └── demo4.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useLockFn │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useLongPress │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ ├── demo2.tsx │ │ │ │ └── demo3.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useMap │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useMemoizedFn │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ └── demo2.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useMount │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useMouse │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ └── demo2.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useMutationObserver │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useNetwork │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── usePagination │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ ├── demo2.tsx │ │ │ │ ├── demo3.tsx │ │ │ │ └── demo4.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ ├── index.zh-CN.md │ │ │ └── types.ts │ │ ├── usePrevious │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ └── demo2.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useRafInterval │ │ │ ├── __tests__ │ │ │ │ ├── index.test.ts │ │ │ │ └── node.ts │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ └── demo2.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useRafState │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useRafTimeout │ │ │ ├── __tests__ │ │ │ │ ├── index.test.ts │ │ │ │ └── node.ts │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ └── demo2.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useReactive │ │ │ ├── __tests__ │ │ │ │ └── index.test.tsx │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ ├── demo2.tsx │ │ │ │ ├── demo3.tsx │ │ │ │ ├── demo4.tsx │ │ │ │ └── index.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useRequest │ │ │ ├── __tests__ │ │ │ │ ├── index.test.ts │ │ │ │ ├── useAutoRunPlugin.test.ts │ │ │ │ ├── useCachePlugin.test.tsx │ │ │ │ ├── useDebouncePlugin.test.ts │ │ │ │ ├── useLoadingDelayPlugin.test.ts │ │ │ │ ├── usePollingPlugin.test.ts │ │ │ │ ├── useRefreshOnWindowFocusPlugin.test.ts │ │ │ │ ├── useRetryPlugin.test.ts │ │ │ │ └── useThrottlePlugin.test.ts │ │ │ ├── doc │ │ │ │ ├── basic │ │ │ │ │ ├── basic.en-US.md │ │ │ │ │ ├── basic.zh-CN.md │ │ │ │ │ └── demo │ │ │ │ │ │ ├── cancel.tsx │ │ │ │ │ │ ├── default.tsx │ │ │ │ │ │ ├── lifeCycle.tsx │ │ │ │ │ │ ├── manual-run.tsx │ │ │ │ │ │ ├── manual-runAsync.tsx │ │ │ │ │ │ ├── mutate.tsx │ │ │ │ │ │ ├── params.tsx │ │ │ │ │ │ └── refresh.tsx │ │ │ │ ├── cache │ │ │ │ │ ├── cache.en-US.md │ │ │ │ │ ├── cache.zh-CN.md │ │ │ │ │ └── demo │ │ │ │ │ │ ├── cacheKey.tsx │ │ │ │ │ │ ├── clearCache.tsx │ │ │ │ │ │ ├── params.tsx │ │ │ │ │ │ ├── setCache.tsx │ │ │ │ │ │ ├── share.tsx │ │ │ │ │ │ └── staleTime.tsx │ │ │ │ ├── debounce │ │ │ │ │ ├── debounce.en-US.md │ │ │ │ │ ├── debounce.zh-CN.md │ │ │ │ │ └── demo │ │ │ │ │ │ └── debounce.tsx │ │ │ │ ├── index │ │ │ │ │ ├── demo │ │ │ │ │ │ ├── default.tsx │ │ │ │ │ │ └── manual.tsx │ │ │ │ │ ├── index.en-US.md │ │ │ │ │ └── index.zh-CN.md │ │ │ │ ├── loadingDelay │ │ │ │ │ ├── demo │ │ │ │ │ │ └── loadingDelay.tsx │ │ │ │ │ ├── loadingDelay.en-US.md │ │ │ │ │ └── loadingDelay.zh-CN.md │ │ │ │ ├── polling │ │ │ │ │ ├── demo │ │ │ │ │ │ ├── polling.tsx │ │ │ │ │ │ └── pollingError.tsx │ │ │ │ │ ├── polling.en-US.md │ │ │ │ │ └── polling.zh-CN.md │ │ │ │ ├── ready │ │ │ │ │ ├── demo │ │ │ │ │ │ ├── manualReady.tsx │ │ │ │ │ │ └── ready.tsx │ │ │ │ │ ├── ready.en-US.md │ │ │ │ │ └── ready.zh-CN.md │ │ │ │ ├── refreshDeps │ │ │ │ │ ├── demo │ │ │ │ │ │ ├── refreshDeps.tsx │ │ │ │ │ │ └── refreshDepsAction.tsx │ │ │ │ │ ├── refresyDeps.en-US.md │ │ │ │ │ └── refresyDeps.zh-CN.md │ │ │ │ ├── refreshOnWindowFocus │ │ │ │ │ ├── demo │ │ │ │ │ │ └── refreshOnWindowFocus.tsx │ │ │ │ │ ├── refreshOnWindowFocus.en-US.md │ │ │ │ │ └── refreshOnWindowFocus.zh-CN.md │ │ │ │ ├── retry │ │ │ │ │ ├── demo │ │ │ │ │ │ └── retry.tsx │ │ │ │ │ ├── retry.en-US.md │ │ │ │ │ └── retry.zh-CN.md │ │ │ │ └── throttle │ │ │ │ │ ├── demo │ │ │ │ │ └── throttle.tsx │ │ │ │ │ ├── throttle.en-US.md │ │ │ │ │ └── throttle.zh-CN.md │ │ │ ├── index.ts │ │ │ └── src │ │ │ │ ├── Fetch.ts │ │ │ │ ├── plugins │ │ │ │ ├── useAutoRunPlugin.ts │ │ │ │ ├── useCachePlugin.ts │ │ │ │ ├── useDebouncePlugin.ts │ │ │ │ ├── useLoadingDelayPlugin.ts │ │ │ │ ├── usePollingPlugin.ts │ │ │ │ ├── useRefreshOnWindowFocusPlugin.ts │ │ │ │ ├── useRetryPlugin.ts │ │ │ │ └── useThrottlePlugin.ts │ │ │ │ ├── types.ts │ │ │ │ ├── useRequest.ts │ │ │ │ ├── useRequestImplement.ts │ │ │ │ └── utils │ │ │ │ ├── cache.ts │ │ │ │ ├── cachePromise.ts │ │ │ │ ├── cacheSubscribe.ts │ │ │ │ ├── isDocumentVisible.ts │ │ │ │ ├── isOnline.ts │ │ │ │ ├── limit.ts │ │ │ │ ├── subscribeFocus.ts │ │ │ │ └── subscribeReVisible.ts │ │ ├── useResetState │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useResponsive │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.test.ts.snap │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useSafeState │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useScroll │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ ├── demo2.tsx │ │ │ │ └── demo3.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useSelections │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ ├── demo2.tsx │ │ │ │ └── demo3.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useSessionStorageState │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useSet │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useSetState │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ └── demo2.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useSize │ │ │ ├── __tests__ │ │ │ │ └── index.test.tsx │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ └── demo2.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useTextSelection │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ ├── demo2.tsx │ │ │ │ └── demo3.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useTheme │ │ │ ├── __test__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useThrottle │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ ├── index.zh-CN.md │ │ │ └── throttleOptions.ts │ │ ├── useThrottleEffect │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useThrottleFn │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useTimeout │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ └── demo2.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useTitle │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useToggle │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ └── demo2.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useTrackedEffect │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useUnmount │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useUnmountedRef │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.tsx │ │ │ └── index.zh-CN.md │ │ ├── useUpdate │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useUpdateEffect │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useUpdateLayoutEffect │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useVirtualList │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ ├── demo1.tsx │ │ │ │ └── demo2.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useWebSocket │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ ├── useWhyDidYouUpdate │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── demo │ │ │ │ └── demo1.tsx │ │ │ ├── index.en-US.md │ │ │ ├── index.ts │ │ │ └── index.zh-CN.md │ │ └── utils │ │ │ ├── __test__ │ │ │ └── index.spec.ts │ │ │ ├── createEffectWithTarget.ts │ │ │ ├── depsAreSame.ts │ │ │ ├── depsEqual.ts │ │ │ ├── domTarget.ts │ │ │ ├── getDocumentOrShadow.ts │ │ │ ├── index.ts │ │ │ ├── isAppleDevice.ts │ │ │ ├── isBrowser.ts │ │ │ ├── isDev.ts │ │ │ ├── lodash-polyfill.ts │ │ │ ├── rect.ts │ │ │ ├── testingHelpers.ts │ │ │ ├── tests.tsx │ │ │ ├── useDeepCompareWithTarget.ts │ │ │ ├── useEffectWithTarget.ts │ │ │ ├── useIsomorphicLayoutEffectWithTarget.ts │ │ │ └── useLayoutEffectWithTarget.ts │ ├── tsconfig.json │ ├── tsconfig.pro.json │ └── webpack.config.js └── use-url-state │ ├── README.md │ ├── demo │ ├── demo1.tsx │ ├── demo2.tsx │ ├── demo3.tsx │ └── demo4.tsx │ ├── gulpfile.js │ ├── package.json │ ├── src │ ├── __tests__ │ │ ├── browser.test.tsx │ │ ├── index.tsx │ │ └── router.test.tsx │ └── index.ts │ ├── tsconfig.json │ ├── tsconfig.pro.json │ ├── use-url-state.en-US.md │ ├── use-url-state.zh-CN.md │ └── webpack.config.js ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── public ├── logo.svg ├── simple-logo.svg ├── style.css └── useExternal │ ├── bootstrap-badge.css │ └── test-external-script.js ├── scripts └── build-with-relative-paths.js ├── tsconfig.json ├── tsconfig.pro.json ├── umd.html └── webpack.common.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@babel/env"], "@babel/react"], 3 | "plugins": ["@babel/plugin-transform-runtime"] 4 | } 5 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-pro 2 | repo_token: dO6l2UfWXIPwCzK3lmEUKQyQsfzXZUZml 3 | -------------------------------------------------------------------------------- /.cursor/rules/demo.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: components/*/demo/** 4 | alwaysApply: false 5 | --- 6 | 7 | # Demo 规范 8 | 9 | - demo 代码尽可能简洁 10 | - 避免冗余代码,方便用户复制到项目直接使用 11 | - 每个 demo 聚焦展示一个功能点 12 | - 提供中英文两个版本的说明 13 | - demo 文件命名: 14 | - 英文 demo: index.en-US.md 15 | - 中文 demo: index.zh-CN.md 16 | - 确保 demo 在各种尺寸下都能正常展示 17 | - 对于复杂交互提供必要的操作说明 18 | 19 | ## 文件组织 20 | 21 | - 每个组件演示包含 `.md`(说明文档)和 `.ts`(实际代码)两部分 22 | - 位置:hooks 目录下的 `src` 子目录,如 `packages/hooks/src/useHover` 23 | - 文件名应简洁地描述示例内容 24 | 25 | ## MD 文档规范 26 | 27 | - 必须包含 `## zh-CN` 和 `## en-US` 两种语言说明 28 | - 内容简洁明了,突出组件特性和用法 29 | - 避免冗长段落,必要时使用列表或粗体 30 | - 标注注意事项和实验性功能 31 | 32 | ## 代码质量 33 | 34 | - 实用且专注于单一功能 35 | - 关键处添加简洁注释 36 | - 使用有意义的数据和变量 37 | - 优先使用 ahooks 内置 hook 或者公共方法,减少外部依赖 38 | 39 | ## 质量要求 40 | 41 | - 确保代码运行正常,无控制台错误 42 | - 适配常见浏览器 43 | - 避免过时 API,及时更新到新推荐用法 44 | -------------------------------------------------------------------------------- /.cursor/rules/docs.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 规范项目文档和 Changelog 3 | globs: ["**/CHANGELOG*.md", "components/**/index.*.md"] 4 | alwaysApply: false 5 | --- 6 | 7 | # Changelog Emoji 规范 8 | 9 | - 🐞 Bug 修复 10 | - 💄 样式更新或 token 更新 11 | - 🆕 新增特性,新增属性 12 | - 🔥 极其值得关注的新增特性 13 | - 🇺🇸🇨🇳🇬🇧 国际化改动 14 | - 📖 📝 文档或网站改进 15 | - ✅ 新增或更新测试用例 16 | - 🛎 更新警告/提示信息 17 | - ⌨️ ♿ 可访问性增强 18 | - 🗑 废弃或移除 19 | - 🛠 重构或工具链优化 20 | - ⚡️ 性能提升 21 | 22 | # 文档规范 23 | 24 | - 提供中英文两个版本 25 | - 新属性需声明可用的版本号 26 | - 属性命名符合 API 命名规则 27 | - hook 文档包含:使用场景、基础用法、API 说明 28 | - 文档示例应简洁明了 29 | - 属性的描述应清晰易懂 30 | - 对复杂功能提供详细说明 31 | - 加入 TypeScript 定义 32 | - 提供常见问题解答 33 | - 更新文档时同步更新中英文版本 34 | 35 | ## 其他要求 36 | 37 | - 新增属性时,建议用易于理解的语言描述用户可以感知的变化 38 | - 存在破坏性改动时,尽量给出原始的 PR 链接,社区提交的 PR 改动加上提交者的链接 39 | -------------------------------------------------------------------------------- /.cursor/rules/project.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | # 项目背景 7 | 8 | 这是由蚂蚁团队开发的一个高质量、可靠的 React Hooks 库。 9 | 10 | - 易学易用 11 | - 支持 SSR 12 | - 对输入输出函数做了特殊处理,避免闭包问题 13 | - 包含大量提炼自业务的高级 Hooks 14 | - 包含丰富的基础 Hooks 15 | - 使用 TypeScript 构建,提供完整的类型定义文件 16 | 17 | # 编码规范 18 | 19 | - 使用 TypeScript 和 React 书写 20 | - 避免引入新依赖,严控打包体积 21 | - 兼容现代浏览器 22 | - 支持服务端渲染 23 | - 保持向下兼容,避免 breaking change 24 | - 合理使用 React.memo、useMemo 和 useCallback 优化性能 25 | -------------------------------------------------------------------------------- /.cursor/rules/testing.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: **/__tests__/**,**/*.test.tsx,**/*.test.ts 4 | alwaysApply: false 5 | --- 6 | # 测试规范 7 | 8 | - 使用 Jest 和 @testing-library/react 编写单元测试 9 | - 测试覆盖率要求 100% 10 | - 测试文件放在 __tests__ 目录,命名格式为:index.test.ts 或 xxx.test.ts 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const fabric = require('@umijs/fabric'); 2 | 3 | module.exports = { 4 | ...fabric.default, 5 | rules: { 6 | ...fabric.default.rules, 7 | 'no-restricted-syntax': 'off', 8 | 'no-plusplus': 'off', 9 | 'no-console': 'off', 10 | 'no-underscore-dangle': 'off', 11 | 'consistent-return': 'off', 12 | '@typescript-eslint/ban-ts-ignore': 'off', 13 | '@typescript-eslint/no-object-literal-type-assertion': 'off', 14 | '@typescript-eslint/no-parameter-properties': 'off', 15 | 'import/no-useless-path-segments': 'off', 16 | 'no-unused-expressions': 'off', 17 | 'react-hooks/rules-of-hooks': 'error', 18 | 'react-hooks/exhaustive-deps': 'off', 19 | 'no-await-in-loop': 'off', 20 | 'no-constant-condition': ['warn', { checkLoops: false }], 21 | }, 22 | plugins: [...fabric.default.plugins, 'react-hooks'], 23 | parserOptions: { 24 | ...fabric.default.parserOptions, 25 | project: './packages/**/tsconfig.json', 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /.github/workflows/comment-when-needs-more-info.yml: -------------------------------------------------------------------------------- 1 | name: Comment When Needs More Info Label Added 2 | 3 | on: 4 | issues: 5 | types: [labeled] 6 | 7 | jobs: 8 | create-comment: 9 | runs-on: ubuntu-latest 10 | if: github.event.label.name == 'needs more info' 11 | steps: 12 | - name: Create comment 13 | uses: actions-cool/issues-helper@v3 14 | with: 15 | actions: 'create-comment' 16 | token: ${{ secrets.GITHUB_TOKEN }} 17 | issue-number: ${{ github.event.issue.number }} 18 | body: | 19 | Hi, ${{ github.event.issue.user.login }}. 20 | 21 | It seems that this issue is a bit vague and lacks some necessary information. 22 | 23 | 看起来这条 issue 描述得有些模糊,缺少一些必要的信息。 24 | -------------------------------------------------------------------------------- /.github/workflows/gitleaks.yml: -------------------------------------------------------------------------------- 1 | name: gitleaks 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | gitleaks: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - name: wget 11 | uses: wei/wget@v1 12 | with: 13 | args: -O .gitleaks.toml https://raw.githubusercontent.com/ycjcl868/gitleaks/master/.gitleaks.toml 14 | - name: gitleaks-action 15 | uses: gitleaks/gitleaks-action@v1.6.0 16 | -------------------------------------------------------------------------------- /.github/workflows/issue-close-require.yml: -------------------------------------------------------------------------------- 1 | name: Issue Close Require 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | jobs: 8 | close-issues: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: need reproduce 12 | uses: actions-cool/issues-helper@v3 13 | with: 14 | actions: 'close-issues' 15 | labels: '🤔 Need Reproduce' 16 | inactive-day: 3 17 | 18 | - name: needs more info 19 | uses: actions-cool/issues-helper@v3 20 | with: 21 | actions: 'close-issues' 22 | labels: 'needs more info' 23 | inactive-day: 3 24 | body: | 25 | Since the issue was labeled with `needs more info`, but no response in 3 days. This issue will be closed. If you have any questions, you can comment and reply. 26 | 由于该 issue 被标记为需要更多信息,却 3 天未收到回应。现关闭 issue,若有任何问题,可评论回复。 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | es 3 | lib 4 | .docz 5 | node_modules 6 | .history 7 | .idea 8 | .vscode 9 | coverage 10 | .doc 11 | .DS_Store 12 | .umi 13 | .umi-production 14 | page 15 | lerna-debug.log 16 | tsconfig.tsbuildinfo 17 | packages/hooks/README.md 18 | yarn-error.log 19 | package-lock.json 20 | metadata.json 21 | .eslintcache 22 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run pretty -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | auto-install-peers=true 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'lts/*' 4 | install: 5 | - pnpm install 6 | - pnpm install -g surge 7 | script: 8 | - pnpm run build:doc 9 | - surge ./dist ahooks-$(git rev-parse --short HEAD).surge.sh 10 | - pnpm run coveralls # generate static files 11 | cache: 12 | directories: 13 | - 'node_modules' 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.zh-CN.MD: -------------------------------------------------------------------------------- 1 | # 贡献指南 2 | 3 | 这篇指南会指导你如何为 `ahooks` 贡献一份自己的力量,请在你要提 issue 或者 pull request 之前花几分钟来阅读一遍这篇指南。 4 | 5 | ## 透明的开发 6 | 7 | 我们所有的工作都会放在 [GitHub](https://github.com/alibaba/hooks) 上。不管是核心团队的成员还是外部贡献者的 pull request 都需要经过同样流程的 review。 8 | 9 | ## 新增功能 10 | 11 | 如果你想新增一个 Hooks,我们建议你先建立一个 issue,说明该 Hooks 的应用场景及用法,参考 [[RFC] useLockFn](https://github.com/alibaba/hooks/issues/562)。 12 | 13 | 然后你可以基于已有 Hook 来初始化一个新的 Hook。 14 | 15 | ## Pull Request 16 | 17 | 我们会关注所有的 pull request,会 review 以及合并你的代码,也有可能要求你做一些修改或者告诉你我们为什么不能接受这样的修改。 18 | 19 | 在你发送 Pull Request 之前,请确认你是按照下面的步骤来做的: 20 | 21 | 1. 基于 master 分支做修改。 22 | 23 | 2. 如果你修复了一个 bug 或者新增了一个功能,请确保写了相应的测试,这很重要。 24 | 25 | 3. 确认所有的测试是通过的 `pnpm run test`。 26 | 27 | ## 开发流程 28 | 29 | 在你 clone 代码并且使用 `pnpm run init` 安装完依赖后,你还可以运行下面几个常用的命令: 30 | 31 | 1. `pnpm start` 在本地运行 `ahooks` 网站。 32 | 33 | 2. `pnpm run test` 运行测试。 34 | 35 | 3. `pnpm run build` 构建编译。 36 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Please report vulnerabilities to brickspert.fjl@antfin.com or guangbo.hgb@alibaba-inc.com 6 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", 3 | "files": { 4 | "ignoreUnknown": true 5 | }, 6 | "vcs": { 7 | "enabled": true, 8 | "clientKind": "git", 9 | "useIgnoreFile": true 10 | }, 11 | "linter": { 12 | "rules": { 13 | "style": { 14 | "noNonNullAssertion": "off" 15 | }, 16 | "correctness": { 17 | "useHookAtTopLevel": "error" 18 | }, 19 | "suspicious": { 20 | "noExplicitAny": "off" 21 | } 22 | } 23 | }, 24 | "formatter": { 25 | "lineWidth": 100, 26 | "indentStyle": "space" 27 | }, 28 | "javascript": { 29 | "parser": { 30 | "unsafeParameterDecoratorsEnabled": true 31 | }, 32 | "formatter": { 33 | "quoteStyle": "single", 34 | "jsxQuoteStyle": "single" 35 | } 36 | }, 37 | "css": { 38 | "parser": { 39 | "cssModules": true 40 | }, 41 | "formatter": { 42 | "enabled": true 43 | }, 44 | "linter": { 45 | "enabled": true 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /dingtalk.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/hooks/37ffafebd972226d015535f70e4d8816f869e213/dingtalk.jpg -------------------------------------------------------------------------------- /docs/guide/index.en-US.md: -------------------------------------------------------------------------------- 1 | ## Intro 2 | 3 | ahooks, pronounced [eɪ hʊks], is a high-quality and reliable React Hooks library. In the current React project development process, a set of easy-to-use React Hooks library is indispensable, hope ahooks can be your choice. 4 | 5 | ## Features 6 | 7 | - Easy to learn and use 8 | - Supports SSR 9 | - Special treatment for functions, avoid closure problems 10 | - Contains a large number of advanced Hooks that are refined from business scenarios 11 | - Contains a comprehensive collection of basic Hooks 12 | - Written in TypeScript with predictable static types 13 | 14 | ## Install 15 | 16 | ```bash 17 | $ npm install --save ahooks 18 | # or 19 | $ yarn add ahooks 20 | # or 21 | $ pnpm add ahooks 22 | # or 23 | $ bun add ahooks 24 | ``` 25 | 26 | ## Usage 27 | 28 | ```ts 29 | import { useRequest } from 'ahooks'; 30 | ``` 31 | 32 | ## Online Demo 33 | 34 | [![Edit demo for ahooks](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/demo-for-ahooks-forked-fg79k?file=/src/App.js) 35 | -------------------------------------------------------------------------------- /docs/guide/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | # 介绍 2 | 3 | ahooks,发音 [eɪ hʊks],是一套高质量可靠的 React Hooks 库。在当前 React 项目研发过程中,一套好用的 React Hooks 库是必不可少的,希望 ahooks 能成为您的选择。 4 | 5 | ## 特性 6 | 7 | - 易学易用 8 | - 支持 SSR 9 | - 对输入输出函数做了特殊处理,且避免闭包问题 10 | - 包含大量提炼自业务的高级 Hooks 11 | - 包含丰富的基础 Hooks 12 | - 使用 TypeScript 构建,提供完整的类型定义文件 13 | 14 | ## 安装 15 | 16 | ```bash 17 | $ npm install --save ahooks 18 | # or 19 | $ yarn add ahooks 20 | # or 21 | $ pnpm add ahooks 22 | # or 23 | $ bun add ahooks 24 | ``` 25 | 26 | ## 使用 27 | 28 | ```ts 29 | import { useRequest } from 'ahooks'; 30 | ``` 31 | 32 | ## 💻 在线体验 33 | 34 | [![Edit demo for ahooks](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/demo-for-ahooks-forked-fg79k?file=/src/App.js) 35 | -------------------------------------------------------------------------------- /example/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/hooks/37ffafebd972226d015535f70e4d8816f869e213/example/.gitkeep -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** esm modules to transform */ 2 | const esmModules = [ 3 | // `query-string` and its related dependencies 4 | 'query-string', 5 | 'decode-uri-component', 6 | 'split-on-first', 7 | 'filter-obj', 8 | ]; 9 | 10 | module.exports = { 11 | preset: 'ts-jest/presets/js-with-ts', 12 | testEnvironment: 'jsdom', 13 | clearMocks: true, 14 | testPathIgnorePatterns: ['/.history/'], 15 | modulePathIgnorePatterns: ['/package.json'], 16 | resetMocks: false, 17 | setupFiles: ['./jest.setup.js', 'jest-localstorage-mock', './match-media-mock.js'], 18 | setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'], 19 | transform: { 20 | '^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.json' }], 21 | }, 22 | collectCoverageFrom: [ 23 | '/**/src/**/*.{js,jsx,ts,tsx}', 24 | '!**/demo/**', 25 | '!**/example/**', 26 | '!**/es/**', 27 | '!**/lib/**', 28 | '!**/dist/**', 29 | ], 30 | transformIgnorePatterns: [`node_modules/(?!(?:.pnpm/)?(${esmModules.join('|')}))`], 31 | }; 32 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | // mock screen full events 2 | // https://github.com/sindresorhus/screenfull/blob/main/index.js 3 | const screenfullMethods = [ 4 | 'requestFullscreen', 5 | 'exitFullscreen', 6 | 'fullscreenElement', 7 | 'fullscreenEnabled', 8 | 'fullscreenchange', 9 | 'fullscreenerror', 10 | ]; 11 | screenfullMethods.forEach((item) => { 12 | document[item] = () => {}; 13 | HTMLElement.prototype[item] = () => {}; 14 | }); 15 | -------------------------------------------------------------------------------- /match-media-mock.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(window, 'matchMedia', { 2 | writable: true, 3 | value: jest.fn().mockImplementation((query) => ({ 4 | matches: false, 5 | media: query, 6 | onchange: null, 7 | addListener: jest.fn(), // deprecated 8 | removeListener: jest.fn(), // deprecated 9 | addEventListener: jest.fn(), 10 | removeEventListener: jest.fn(), 11 | dispatchEvent: jest.fn(), 12 | })), 13 | }); 14 | -------------------------------------------------------------------------------- /packages/hooks/src/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as ahooks from '..'; 2 | 3 | describe('ahooks', () => { 4 | test('exports modules should be defined', () => { 5 | Object.keys(ahooks).forEach((module) => { 6 | expect(ahooks[module]).toBeDefined(); 7 | }); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/hooks/src/createDeepCompareEffect/index.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import type { DependencyList, useEffect, useLayoutEffect } from 'react'; 3 | import { depsEqual } from '../utils/depsEqual'; 4 | 5 | type EffectHookType = typeof useEffect | typeof useLayoutEffect; 6 | type CreateUpdateEffect = (hook: EffectHookType) => EffectHookType; 7 | 8 | export const createDeepCompareEffect: CreateUpdateEffect = (hook) => (effect, deps) => { 9 | const ref = useRef(); 10 | const signalRef = useRef(0); 11 | 12 | if (deps === undefined || !depsEqual(deps, ref.current)) { 13 | signalRef.current += 1; 14 | } 15 | ref.current = deps; 16 | 17 | hook(effect, [signalRef.current]); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/hooks/src/createUpdateEffect/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react'; 2 | import { useEffect, useLayoutEffect } from 'react'; 3 | import { createUpdateEffect } from '../index'; 4 | 5 | describe('createUpdateEffect', () => { 6 | it('should work for useEffect', () => { 7 | const useUpdateEffect = createUpdateEffect(useEffect); 8 | 9 | let mountedState = 1; 10 | const hook = renderHook(() => 11 | useUpdateEffect(() => { 12 | mountedState = 2; 13 | }), 14 | ); 15 | expect(mountedState).toBe(1); 16 | hook.rerender(); 17 | expect(mountedState).toBe(2); 18 | }); 19 | 20 | it('should work for useLayoutEffect', () => { 21 | const useUpdateLayoutEffect = createUpdateEffect(useLayoutEffect); 22 | 23 | let mountedState = 1; 24 | const hook = renderHook(() => 25 | useUpdateLayoutEffect(() => { 26 | mountedState = 2; 27 | }), 28 | ); 29 | expect(mountedState).toBe(1); 30 | hook.rerender(); 31 | expect(mountedState).toBe(2); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/hooks/src/createUpdateEffect/index.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import type { useEffect, useLayoutEffect } from 'react'; 3 | 4 | type EffectHookType = typeof useEffect | typeof useLayoutEffect; 5 | 6 | export const createUpdateEffect: (hook: EffectHookType) => EffectHookType = 7 | (hook) => (effect, deps) => { 8 | const isMounted = useRef(false); 9 | 10 | // for react-refresh 11 | hook(() => { 12 | return () => { 13 | isMounted.current = false; 14 | }; 15 | }, []); 16 | 17 | hook(() => { 18 | if (!isMounted.current) { 19 | isMounted.current = true; 20 | } else { 21 | return effect(); 22 | } 23 | }, deps); 24 | }; 25 | 26 | export default createUpdateEffect; 27 | -------------------------------------------------------------------------------- /packages/hooks/src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.jpg'; 2 | 3 | interface Window { 4 | TEST_SCRIPT?: any; 5 | } 6 | -------------------------------------------------------------------------------- /packages/hooks/src/useAsyncEffect/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Default usage 3 | * desc: Do async check when component is mounted. 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 组件加载时进行异步的检查 7 | */ 8 | 9 | import { useAsyncEffect } from 'ahooks'; 10 | import React, { useState } from 'react'; 11 | 12 | function mockCheck(): Promise { 13 | return new Promise((resolve) => { 14 | setTimeout(() => { 15 | resolve(true); 16 | }, 3000); 17 | }); 18 | } 19 | 20 | export default () => { 21 | const [pass, setPass] = useState(); 22 | 23 | useAsyncEffect(async () => { 24 | setPass(await mockCheck()); 25 | }, []); 26 | 27 | return ( 28 |
29 | {pass === undefined && 'Checking...'} 30 | {pass === true && 'Check passed.'} 31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/hooks/src/useAsyncEffect/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useAsyncEffect 7 | 8 | useEffect support async function. 9 | 10 | ## 代码演示 11 | 12 | ### Default usage 13 | 14 | 15 | 16 | ### Break off 17 | 18 | 19 | 20 | ## API 21 | 22 | ```typescript 23 | function useAsyncEffect( 24 | effect: () => AsyncGenerator | Promise, 25 | deps: DependencyList 26 | ); 27 | ``` 28 | -------------------------------------------------------------------------------- /packages/hooks/src/useAsyncEffect/index.ts: -------------------------------------------------------------------------------- 1 | import type { DependencyList } from 'react'; 2 | import { useEffect } from 'react'; 3 | import { isFunction } from '../utils'; 4 | 5 | function isAsyncGenerator( 6 | val: AsyncGenerator | Promise, 7 | ): val is AsyncGenerator { 8 | return isFunction(val[Symbol.asyncIterator]); 9 | } 10 | 11 | function useAsyncEffect( 12 | effect: () => AsyncGenerator | Promise, 13 | deps?: DependencyList, 14 | ) { 15 | useEffect(() => { 16 | const e = effect(); 17 | let cancelled = false; 18 | async function execute() { 19 | if (isAsyncGenerator(e)) { 20 | while (true) { 21 | const result = await e.next(); 22 | if (result.done || cancelled) { 23 | break; 24 | } 25 | } 26 | } else { 27 | await e; 28 | } 29 | } 30 | execute(); 31 | return () => { 32 | cancelled = true; 33 | }; 34 | }, deps); 35 | } 36 | 37 | export default useAsyncEffect; 38 | -------------------------------------------------------------------------------- /packages/hooks/src/useAsyncEffect/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useAsyncEffect 7 | 8 | useEffect 支持异步函数。 9 | 10 | ## 代码演示 11 | 12 | ### 基础用法 13 | 14 | 15 | 16 | ### 中断执行 17 | 18 | 19 | 20 | ## API 21 | 22 | ```typescript 23 | function useAsyncEffect( 24 | effect: () => AsyncGenerator | Promise, 25 | deps: DependencyList 26 | ); 27 | ``` 28 | -------------------------------------------------------------------------------- /packages/hooks/src/useBoolean/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Basic usage 3 | * desc: Toggle boolean, default value can be set optionally. 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 切换 boolean,可以接收默认值。 7 | */ 8 | 9 | import React from 'react'; 10 | import { useBoolean } from 'ahooks'; 11 | 12 | export default () => { 13 | const [state, { toggle, setTrue, setFalse }] = useBoolean(true); 14 | 15 | return ( 16 |
17 |

Effects:{JSON.stringify(state)}

18 |

19 | 22 | 25 | 28 |

29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /packages/hooks/src/useBoolean/index.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import useToggle from '../useToggle'; 3 | 4 | export interface Actions { 5 | setTrue: () => void; 6 | setFalse: () => void; 7 | set: (value: boolean) => void; 8 | toggle: () => void; 9 | } 10 | 11 | export default function useBoolean(defaultValue = false): [boolean, Actions] { 12 | const [state, { toggle, set }] = useToggle(!!defaultValue); 13 | 14 | const actions: Actions = useMemo(() => { 15 | const setTrue = () => set(true); 16 | const setFalse = () => set(false); 17 | return { 18 | toggle, 19 | set: (v) => set(!!v), 20 | setTrue, 21 | setFalse, 22 | }; 23 | }, []); 24 | 25 | return [state, actions]; 26 | } 27 | -------------------------------------------------------------------------------- /packages/hooks/src/useClickAway/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Default usage 3 | * desc: Please click button or outside of button to show effects. 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 请点击按钮或按钮外查看效果。 7 | */ 8 | 9 | import React, { useState, useRef } from 'react'; 10 | import { useClickAway } from 'ahooks'; 11 | 12 | export default () => { 13 | const [counter, setCounter] = useState(0); 14 | const ref = useRef(null); 15 | useClickAway(() => { 16 | setCounter((s) => s + 1); 17 | }, ref); 18 | 19 | return ( 20 |
21 | 24 |

counter: {counter}

25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /packages/hooks/src/useClickAway/demo/demo2.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Support DOM 3 | * desc: Support pass in a DOM element or function. 4 | * 5 | * title.zh-CN: 支持传入 DOM 6 | * desc.zh-CN: 支持直接传入 DOM 对象或 function。 7 | */ 8 | 9 | import React, { useState } from 'react'; 10 | import { useClickAway } from 'ahooks'; 11 | 12 | export default () => { 13 | const [counter, setCounter] = useState(0); 14 | 15 | useClickAway( 16 | () => { 17 | setCounter((s) => s + 1); 18 | }, 19 | () => document.getElementById('use-click-away-button'), 20 | ); 21 | 22 | return ( 23 |
24 | 27 |

counter: {counter}

28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /packages/hooks/src/useClickAway/demo/demo3.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Support multiple DOM 3 | * desc: Support pass multiple DOM elements. 4 | * 5 | * title.zh-CN: 支持多个 DOM 对象 6 | * desc.zh-CN: 支持传入多个目标对象。 7 | */ 8 | 9 | import React, { useState, useRef } from 'react'; 10 | import { useClickAway } from 'ahooks'; 11 | 12 | export default () => { 13 | const [counter, setCounter] = useState(0); 14 | const ref1 = useRef(null); 15 | const ref2 = useRef(null); 16 | useClickAway(() => { 17 | setCounter((s) => s + 1); 18 | }, [ref1, ref2]); 19 | 20 | return ( 21 |
22 | 25 | 28 |

counter: {counter}

29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /packages/hooks/src/useClickAway/demo/demo4.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Listen to other events 3 | * desc: By setting eventName, you can specify the event to be listened, Try click the right mouse. 4 | * 5 | * title.zh-CN: 监听其它事件 6 | * desc.zh-CN: 通过设置 eventName,可以指定需要监听的事件,试试点击鼠标右键。 7 | */ 8 | 9 | import React, { useState, useRef } from 'react'; 10 | import { useClickAway } from 'ahooks'; 11 | 12 | export default () => { 13 | const [counter, setCounter] = useState(0); 14 | const ref = useRef(null); 15 | useClickAway( 16 | () => { 17 | setCounter((s) => s + 1); 18 | }, 19 | ref, 20 | 'contextmenu', 21 | ); 22 | 23 | return ( 24 |
25 | 28 |

counter: {counter}

29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /packages/hooks/src/useClickAway/demo/demo5.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Support multiple events 3 | * desc: Set up multiple events, you can try using the mouse click or right click. 4 | * 5 | * title.zh-CN: 支持传入多个事件名称 6 | * desc.zh-CN: 设置了多个事件,你可以试试用鼠标左键或者右键。 7 | */ 8 | 9 | import React, { useState, useRef } from 'react'; 10 | import { useClickAway } from 'ahooks'; 11 | 12 | export default () => { 13 | const [counter, setCounter] = useState(0); 14 | const ref = useRef(null); 15 | useClickAway( 16 | () => { 17 | setCounter((s) => s + 1); 18 | }, 19 | ref, 20 | ['click', 'contextmenu'], 21 | ); 22 | 23 | return ( 24 |
25 | 28 |

counter: {counter}

29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /packages/hooks/src/useClickAway/demo/demo6.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Support shadow DOM 3 | * desc: Add the addEventListener to shadow DOM root instead of the document 4 | * 5 | * title.zh-CN: 支持 shadow DOM 6 | * desc.zh-CN: 将 addEventListener 添加到 shadow DOM root 7 | */ 8 | 9 | import React, { useState, useRef } from 'react'; 10 | import { useClickAway } from 'ahooks'; 11 | import root from 'react-shadow'; 12 | 13 | export default () => { 14 | const [counter, setCounter] = useState(0); 15 | const ref = useRef(null); 16 | useClickAway( 17 | () => { 18 | setCounter((s) => s + 1); 19 | }, 20 | ref, 21 | ['click', 'contextmenu'], 22 | ); 23 | 24 | return ( 25 | 26 |
27 | 30 |

counter: {counter}

31 |
32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /packages/hooks/src/useControllableValue/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Uncontrolled component 3 | * desc: If there is no value in props, the component manage state by self 4 | * 5 | * title.zh-CN: 非受控组件 6 | * desc.zh-CN: 如果 props 中没有 value,则组件内部自己管理 state 7 | */ 8 | import React from 'react'; 9 | import { useControllableValue } from 'ahooks'; 10 | 11 | export default (props: any) => { 12 | const [state, setState] = useControllableValue(props, { 13 | defaultValue: '', 14 | }); 15 | 16 | return ( 17 | <> 18 | setState(e.target.value)} style={{ width: 300 }} /> 19 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/hooks/src/useControllableValue/demo/demo2.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Controlled component 3 | * desc: If props has the value field, then the state is controlled by it's parent 4 | * 5 | * title.zh-CN: 受控组件 6 | * desc.zh-CN: 如果 props 有 value 字段,则由父级接管控制 state 7 | */ 8 | 9 | import React, { useState } from 'react'; 10 | import { useControllableValue } from 'ahooks'; 11 | 12 | const ControllableComponent = (props: any) => { 13 | const [state, setState] = useControllableValue(props); 14 | 15 | return setState(e.target.value)} style={{ width: 300 }} />; 16 | }; 17 | 18 | const Parent = () => { 19 | const [state, setState] = useState(''); 20 | const clear = () => { 21 | setState(''); 22 | }; 23 | 24 | return ( 25 | <> 26 | 27 | 30 | 31 | ); 32 | }; 33 | export default Parent; 34 | -------------------------------------------------------------------------------- /packages/hooks/src/useControllableValue/demo/demo3.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: No value, have onChange component 3 | * desc: If there is an onChange field in props, the onChange will be trigger when state change 4 | * 5 | * title.zh-CN: 无 value,有 onChange 的组件 6 | * desc.zh-CN: 只要 props 中有 onChange 字段,则在 state 变化时,就会触发 onChange 函数 7 | */ 8 | 9 | import React, { useState } from 'react'; 10 | import { useControllableValue } from 'ahooks'; 11 | 12 | const ControllableComponent = (props: any) => { 13 | const [state, setState] = useControllableValue(props); 14 | 15 | return ( 16 | { 19 | setState(e.target.value); 20 | }} 21 | style={{ width: 300 }} 22 | /> 23 | ); 24 | }; 25 | const Parent = () => { 26 | const [state, setState] = useState(0); 27 | 28 | return ( 29 | <> 30 |
state:{state}
31 | 32 | 33 | ); 34 | }; 35 | export default Parent; 36 | -------------------------------------------------------------------------------- /packages/hooks/src/useCookieState/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Store state into Cookie 3 | * desc: Refresh this page and you will get the state from Cookie. 4 | * 5 | * title.zh-CN: 将 state 存储在 Cookie 中 6 | * desc.zh-CN: 刷新页面后,可以看到输入框中的内容被从 Cookie 中恢复了。 7 | */ 8 | 9 | import React from 'react'; 10 | import { useCookieState } from 'ahooks'; 11 | 12 | export default () => { 13 | const [message, setMessage] = useCookieState('useCookieStateString'); 14 | return ( 15 | setMessage(e.target.value)} 19 | style={{ width: 300 }} 20 | /> 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/hooks/src/useCountDown/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Basic Usage 3 | * desc: Basic countdown management. 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 基础的倒计时管理。 7 | */ 8 | 9 | import React from 'react'; 10 | import { useCountDown } from 'ahooks'; 11 | 12 | export default () => { 13 | const [countdown, formattedRes] = useCountDown({ 14 | targetDate: `${new Date().getFullYear()}-12-31 23:59:59`, 15 | }); 16 | const { days, hours, minutes, seconds, milliseconds } = formattedRes; 17 | 18 | return ( 19 |

20 | There are {days} days {hours} hours {minutes} minutes {seconds} seconds {milliseconds}{' '} 21 | milliseconds until {new Date().getFullYear()}-12-31 23:59:59 22 |

23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/hooks/src/useCountDown/demo/demo3.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: The rest of time 3 | * desc: A countdown to the number of milliseconds remaining. 4 | * 5 | * title.zh-CN: 剩余时间 6 | * desc.zh-CN: 剩余时间毫秒数的倒计时 7 | */ 8 | 9 | import React from 'react'; 10 | import { useCountDown } from 'ahooks'; 11 | 12 | const App: React.FC = () => { 13 | const [countdown] = useCountDown({ leftTime: 60 * 1000 }); 14 | return

{countdown}

; 15 | }; 16 | 17 | export default App; 18 | -------------------------------------------------------------------------------- /packages/hooks/src/useCreation/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '@testing-library/react'; 2 | import { useState } from 'react'; 3 | import useCreation from '../index'; 4 | 5 | describe('useCreation', () => { 6 | class Foo { 7 | constructor() { 8 | this.data = Math.random(); 9 | } 10 | 11 | data: number; 12 | } 13 | 14 | const setUp = (): any => 15 | renderHook(() => { 16 | const [count, setCount] = useState(0); 17 | const [, setFlag] = useState({}); 18 | const foo = useCreation(() => new Foo(), [count]); 19 | return { 20 | foo, 21 | setCount, 22 | count, 23 | setFlag, 24 | }; 25 | }); 26 | 27 | it('should work', () => { 28 | const hook = setUp(); 29 | const { foo } = hook.result.current; 30 | act(() => { 31 | hook.result.current.setFlag({}); 32 | }); 33 | expect(hook.result.current.foo).toBe(foo); 34 | act(() => { 35 | hook.result.current.setCount(1); 36 | }); 37 | expect(hook.result.current.foo).not.toBe(foo); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /packages/hooks/src/useCreation/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Make sure only one instance is created 3 | * desc: You can click the "Rerender" button and trigger the update of this component. But the instance of Foo will not change. 4 | * 5 | * title.zh-CN: 确保实例不会被重复创建 6 | * desc.zh-CN: 点击 "Rerender" 按钮,触发组件的更新,但 Foo 的实例会保持不变 7 | */ 8 | 9 | import React, { useState } from 'react'; 10 | import { useCreation } from 'ahooks'; 11 | 12 | class Foo { 13 | constructor() { 14 | this.data = Math.random(); 15 | } 16 | 17 | data: number; 18 | } 19 | 20 | export default function () { 21 | const foo = useCreation(() => new Foo(), []); 22 | const [, setFlag] = useState({}); 23 | return ( 24 | <> 25 |

{foo.data}

26 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /packages/hooks/src/useCreation/index.ts: -------------------------------------------------------------------------------- 1 | import type { DependencyList } from 'react'; 2 | import { useRef } from 'react'; 3 | import depsAreSame from '../utils/depsAreSame'; 4 | 5 | export default function useCreation(factory: () => T, deps: DependencyList) { 6 | const { current } = useRef({ 7 | deps, 8 | obj: undefined as undefined | T, 9 | initialized: false, 10 | }); 11 | if (current.initialized === false || !depsAreSame(current.deps, deps)) { 12 | current.deps = deps; 13 | current.obj = factory(); 14 | current.initialized = true; 15 | } 16 | return current.obj as T; 17 | } 18 | -------------------------------------------------------------------------------- /packages/hooks/src/useDebounce/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { act, renderHook } from '@testing-library/react'; 2 | import useDebounce from '../index'; 3 | import { sleep } from '../../utils/testingHelpers'; 4 | 5 | describe('useDebounce', () => { 6 | it('useDebounce wait:200ms', async () => { 7 | let mountedState = 0; 8 | const { result, rerender } = renderHook(() => useDebounce(mountedState, { wait: 200 })); 9 | expect(result.current).toBe(0); 10 | 11 | mountedState = 1; 12 | rerender(); 13 | await sleep(50); 14 | expect(result.current).toBe(0); 15 | 16 | mountedState = 2; 17 | rerender(); 18 | await sleep(100); 19 | expect(result.current).toBe(0); 20 | 21 | mountedState = 3; 22 | rerender(); 23 | await sleep(150); 24 | expect(result.current).toBe(0); 25 | 26 | mountedState = 4; 27 | rerender(); 28 | await act(async () => { 29 | await sleep(250); 30 | }); 31 | expect(result.current).toBe(4); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/hooks/src/useDebounce/debounceOptions.ts: -------------------------------------------------------------------------------- 1 | export interface DebounceOptions { 2 | wait?: number; 3 | leading?: boolean; 4 | trailing?: boolean; 5 | maxWait?: number; 6 | } 7 | -------------------------------------------------------------------------------- /packages/hooks/src/useDebounce/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Default usage 3 | * desc: DebouncedValue will change after the input ends 500ms. 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: DebouncedValue 只会在输入结束 500ms 后变化。 7 | */ 8 | 9 | import React, { useState } from 'react'; 10 | import { useDebounce } from 'ahooks'; 11 | 12 | export default () => { 13 | const [value, setValue] = useState(); 14 | const debouncedValue = useDebounce(value, { wait: 500 }); 15 | 16 | return ( 17 |
18 | setValue(e.target.value)} 21 | placeholder="Typed value" 22 | style={{ width: 280 }} 23 | /> 24 |

DebouncedValue: {debouncedValue}

25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /packages/hooks/src/useDebounce/index.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import useDebounceFn from '../useDebounceFn'; 3 | import type { DebounceOptions } from './debounceOptions'; 4 | 5 | function useDebounce(value: T, options?: DebounceOptions) { 6 | const [debounced, setDebounced] = useState(value); 7 | 8 | const { run } = useDebounceFn(() => { 9 | setDebounced(value); 10 | }, options); 11 | 12 | useEffect(() => { 13 | run(); 14 | }, [value]); 15 | 16 | return debounced; 17 | } 18 | 19 | export default useDebounce; 20 | -------------------------------------------------------------------------------- /packages/hooks/src/useDebounce/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useDebounce 7 | 8 | 用来处理防抖值的 Hook。 9 | 10 | ## 代码演示 11 | 12 | ### 基础用法 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | const debouncedValue = useDebounce( 20 | value: any, 21 | options?: Options 22 | ); 23 | ``` 24 | 25 | ### Params 26 | 27 | | 参数 | 说明 | 类型 | 默认值 | 28 | | ------- | -------------- | --------- | ------ | 29 | | value | 需要防抖的值 | `any` | - | 30 | | options | 配置防抖的行为 | `Options` | - | 31 | 32 | ### Options 33 | 34 | | 参数 | 说明 | 类型 | 默认值 | 35 | | -------- | ------------------------ | --------- | ------- | 36 | | wait | 超时时间,单位为毫秒 | `number` | `1000` | 37 | | leading | 是否在延迟开始前调用函数 | `boolean` | `false` | 38 | | trailing | 是否在延迟开始后调用函数 | `boolean` | `true` | 39 | | maxWait | 最大等待时间,单位为毫秒 | `number` | - | 40 | -------------------------------------------------------------------------------- /packages/hooks/src/useDebounceEffect/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | import { useDebounceEffect } from 'ahooks'; 2 | import React, { useState } from 'react'; 3 | 4 | export default () => { 5 | const [value, setValue] = useState('hello'); 6 | const [records, setRecords] = useState([]); 7 | useDebounceEffect( 8 | () => { 9 | setRecords((val) => [...val, value]); 10 | }, 11 | [value], 12 | { 13 | wait: 1000, 14 | }, 15 | ); 16 | return ( 17 |
18 | setValue(e.target.value)} 21 | placeholder="Typed value" 22 | style={{ width: 280 }} 23 | /> 24 |

25 |

    26 | {records.map((record, index) => ( 27 |
  • {record}
  • 28 | ))} 29 |
30 |

31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/hooks/src/useDebounceEffect/index.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import type { DependencyList, EffectCallback } from 'react'; 3 | import type { DebounceOptions } from '../useDebounce/debounceOptions'; 4 | import useDebounceFn from '../useDebounceFn'; 5 | import useUpdateEffect from '../useUpdateEffect'; 6 | 7 | function useDebounceEffect( 8 | effect: EffectCallback, 9 | deps?: DependencyList, 10 | options?: DebounceOptions, 11 | ) { 12 | const [flag, setFlag] = useState({}); 13 | 14 | const { run } = useDebounceFn(() => { 15 | setFlag({}); 16 | }, options); 17 | 18 | useEffect(() => { 19 | return run(); 20 | }, deps); 21 | 22 | useUpdateEffect(effect, [flag]); 23 | } 24 | 25 | export default useDebounceEffect; 26 | -------------------------------------------------------------------------------- /packages/hooks/src/useDebounceFn/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Default usage 3 | * desc: Frequent calls run, but the function is executed only after all the clicks have completed 500ms. 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 频繁调用 run,但只会在所有点击完成 500ms 后执行一次相关函数 7 | */ 8 | 9 | import { useDebounceFn } from 'ahooks'; 10 | import React, { useState } from 'react'; 11 | 12 | export default () => { 13 | const [value, setValue] = useState(0); 14 | const { run } = useDebounceFn( 15 | () => { 16 | setValue(value + 1); 17 | }, 18 | { 19 | wait: 500, 20 | }, 21 | ); 22 | 23 | return ( 24 |
25 |

Clicked count: {value}

26 | 29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /packages/hooks/src/useDeepCompareEffect/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '@testing-library/react'; 2 | import { useState } from 'react'; 3 | import useDeepCompareEffect from '../index'; 4 | 5 | describe('useDeepCompareEffect', () => { 6 | it('test deep compare', async () => { 7 | const hook = renderHook(() => { 8 | const [x, setX] = useState(0); 9 | const [y, setY] = useState({}); 10 | useDeepCompareEffect(() => { 11 | setX((prevState) => prevState + 1); 12 | }, [y]); 13 | return { x, setY }; 14 | }); 15 | expect(hook.result.current.x).toBe(1); 16 | 17 | await act(async () => { 18 | hook.result.current.setY({}); 19 | }); 20 | expect(hook.result.current.x).toBe(1); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/hooks/src/useDeepCompareEffect/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | import { useDeepCompareEffect } from 'ahooks'; 2 | import React, { useEffect, useState, useRef } from 'react'; 3 | 4 | export default () => { 5 | const [count, setCount] = useState(0); 6 | const effectCountRef = useRef(0); 7 | const deepCompareCountRef = useRef(0); 8 | 9 | useEffect(() => { 10 | effectCountRef.current += 1; 11 | }, [{}]); 12 | 13 | useDeepCompareEffect(() => { 14 | deepCompareCountRef.current += 1; 15 | return () => { 16 | // do something 17 | }; 18 | }, [{}]); 19 | 20 | return ( 21 |
22 |

effectCount: {effectCountRef.current}

23 |

deepCompareCount: {deepCompareCountRef.current}

24 |

25 | 28 |

29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /packages/hooks/src/useDeepCompareEffect/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useDeepCompareEffect 7 | 8 | Usage is the same as `useEffect`, but deps are compared with [react-fast-compare](https://www.npmjs.com/package/react-fast-compare). 9 | 10 | ## Examples 11 | 12 | ### Default usage 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | useDeepCompareEffect( 20 | effect: React.EffectCallback, 21 | deps: React.DependencyList 22 | ); 23 | ``` 24 | -------------------------------------------------------------------------------- /packages/hooks/src/useDeepCompareEffect/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { createDeepCompareEffect } from '../createDeepCompareEffect'; 3 | 4 | export default createDeepCompareEffect(useEffect); 5 | -------------------------------------------------------------------------------- /packages/hooks/src/useDeepCompareEffect/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useDeepCompareEffect 7 | 8 | 用法与 useEffect 一致,但 deps 通过 [react-fast-compare](https://www.npmjs.com/package/react-fast-compare) 进行深比较。 9 | 10 | ## 代码演示 11 | 12 | ### 基础用法 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | useDeepCompareEffect( 20 | effect: React.EffectCallback, 21 | deps: React.DependencyList 22 | ); 23 | ``` 24 | -------------------------------------------------------------------------------- /packages/hooks/src/useDeepCompareLayoutEffect/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '@testing-library/react'; 2 | import { useState } from 'react'; 3 | import useDeepCompareLayoutEffect from '../index'; 4 | 5 | describe('useDeepCompareLayoutEffect', () => { 6 | it('test deep compare', async () => { 7 | const hook = renderHook(() => { 8 | const [x, setX] = useState(0); 9 | const [y, setY] = useState({}); 10 | useDeepCompareLayoutEffect(() => { 11 | setX((x) => x + 1); 12 | }, [y]); 13 | return { x, setY }; 14 | }); 15 | expect(hook.result.current.x).toBe(1); 16 | 17 | await act(async () => { 18 | hook.result.current.setY({}); 19 | }); 20 | expect(hook.result.current.x).toBe(1); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/hooks/src/useDeepCompareLayoutEffect/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | import { useDeepCompareLayoutEffect } from 'ahooks'; 2 | import React, { useLayoutEffect, useState, useRef } from 'react'; 3 | 4 | export default () => { 5 | const [, setCount] = useState(0); 6 | const effectCountRef = useRef(0); 7 | const deepCompareCountRef = useRef(0); 8 | 9 | useLayoutEffect(() => { 10 | effectCountRef.current += 1; 11 | }, [{}]); 12 | 13 | useDeepCompareLayoutEffect(() => { 14 | deepCompareCountRef.current += 1; 15 | return () => { 16 | // do something 17 | }; 18 | }, [{}]); 19 | 20 | return ( 21 |
22 |

effectCount: {effectCountRef.current}

23 |

deepCompareCount: {deepCompareCountRef.current}

24 |

25 | 28 |

29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /packages/hooks/src/useDeepCompareLayoutEffect/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useDeepCompareLayoutEffect 7 | 8 | Usage is the same as `useLayoutEffect`, but deps are compared with [react-fast-compare](https://www.npmjs.com/package/react-fast-compare). 9 | 10 | ## Examples 11 | 12 | ### Default usage 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | useDeepCompareLayoutEffect( 20 | effect: React.EffectCallback, 21 | deps: React.DependencyList 22 | ); 23 | ``` 24 | -------------------------------------------------------------------------------- /packages/hooks/src/useDeepCompareLayoutEffect/index.tsx: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect } from 'react'; 2 | import { createDeepCompareEffect } from '../createDeepCompareEffect'; 3 | 4 | export default createDeepCompareEffect(useLayoutEffect); 5 | -------------------------------------------------------------------------------- /packages/hooks/src/useDeepCompareLayoutEffect/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useDeepCompareLayoutEffect 7 | 8 | 用法与 useLayoutEffect 一致,但 deps 通过 [react-fast-compare](https://www.npmjs.com/package/react-fast-compare) 进行深比较。 9 | 10 | ## 代码演示 11 | 12 | ### 基础用法 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | useDeepCompareLayoutEffect( 20 | effect: React.EffectCallback, 21 | deps: React.DependencyList 22 | ); 23 | ``` 24 | -------------------------------------------------------------------------------- /packages/hooks/src/useDocumentVisibility/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Default usage 3 | * desc: Listen to document visibility change. 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 监听 document 的可见状态 7 | */ 8 | 9 | import React, { useEffect } from 'react'; 10 | import { useDocumentVisibility } from 'ahooks'; 11 | 12 | export default () => { 13 | const documentVisibility = useDocumentVisibility(); 14 | 15 | useEffect(() => { 16 | console.log(`Current document visibility state: ${documentVisibility}`); 17 | }, [documentVisibility]); 18 | 19 | return
Current document visibility state: {documentVisibility}
; 20 | }; 21 | -------------------------------------------------------------------------------- /packages/hooks/src/useDocumentVisibility/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useDocumentVisibility 7 | 8 | A Hook can tell if the page is visible, refer to [visibilityState API](https://developer.mozilla.org/docs/Web/API/Document/visibilityState) 9 | 10 | ## Examples 11 | 12 | 13 | 14 | ## API 15 | 16 | ```typescript 17 | const documentVisibility = useDocumentVisibility(); 18 | ``` 19 | 20 | ### Result 21 | 22 | | Property | Description | Type | 23 | | ------------------ | ------------------------------- | -------------------------------------------------- | 24 | | documentVisibility | Whether the document is visible | `visible`\| `hidden` \| `prerender` \| `undefined` | 25 | -------------------------------------------------------------------------------- /packages/hooks/src/useDocumentVisibility/index.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import useEventListener from '../useEventListener'; 3 | import isBrowser from '../utils/isBrowser'; 4 | 5 | type VisibilityState = 'hidden' | 'visible' | 'prerender' | undefined; 6 | 7 | const getVisibility = () => { 8 | if (!isBrowser) { 9 | return 'visible'; 10 | } 11 | return document.visibilityState; 12 | }; 13 | 14 | function useDocumentVisibility(): VisibilityState { 15 | const [documentVisibility, setDocumentVisibility] = useState(getVisibility); 16 | 17 | useEventListener( 18 | 'visibilitychange', 19 | () => { 20 | setDocumentVisibility(getVisibility()); 21 | }, 22 | { 23 | target: () => document, 24 | }, 25 | ); 26 | 27 | return documentVisibility; 28 | } 29 | 30 | export default useDocumentVisibility; 31 | -------------------------------------------------------------------------------- /packages/hooks/src/useDocumentVisibility/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useDocumentVisibility 7 | 8 | 监听页面是否可见,参考 [visibilityState API](https://developer.mozilla.org/docs/Web/API/Document/visibilityState) 9 | 10 | ## 代码演示 11 | 12 | ### 基础用法 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | const documentVisibility = useDocumentVisibility(); 20 | ``` 21 | 22 | ### Result 23 | 24 | | 参数 | 说明 | 类型 | 25 | | ------------------ | ------------------------------ | -------------------------------------------------- | 26 | | documentVisibility | 判断 document 是否处于可见状态 | `visible`\| `hidden` \| `prerender` \| `undefined` | 27 | -------------------------------------------------------------------------------- /packages/hooks/src/useDrop/demo/demo2.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Customize Image 3 | * desc: Customize image that follow the mouse pointer during dragging. 4 | * 5 | * title.zh-CN: 自定义拖拽图像 6 | * desc.zh-CN: 自定义拖拽过程中跟随鼠标指针的图像。 7 | */ 8 | 9 | import React, { useRef } from 'react'; 10 | import { useDrag } from 'ahooks'; 11 | 12 | const COMMON_STYLE: React.CSSProperties = { 13 | border: '1px solid #e8e8e8', 14 | height: '50px', 15 | lineHeight: '50px', 16 | padding: '16px', 17 | textAlign: 'center', 18 | marginRight: '16px', 19 | }; 20 | 21 | export default () => { 22 | const dragRef = useRef(null); 23 | 24 | useDrag('', dragRef, { 25 | dragImage: { 26 | image: '/logo.svg', 27 | }, 28 | }); 29 | 30 | return ( 31 |
32 | 33 |
drag me
34 |
35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /packages/hooks/src/useEventEmitter/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '@testing-library/react'; 2 | import { useState } from 'react'; 3 | import useEventEmitter from '../index'; 4 | 5 | describe('useEventEmitter', () => { 6 | const setUp = (): any => 7 | renderHook(() => { 8 | const event$ = useEventEmitter(); 9 | const [count, setCount] = useState(0); 10 | event$.useSubscription((val) => { 11 | setCount((c) => c + val); 12 | }); 13 | event$.useSubscription((val) => { 14 | setCount((c) => c + val + 10); 15 | }); 16 | return { 17 | event$, 18 | count, 19 | }; 20 | }); 21 | 22 | it('emit and subscribe should work', () => { 23 | const hook = setUp(); 24 | act(() => { 25 | hook.result.current.event$.emit(1); 26 | }); 27 | expect(hook.result.current.count).toBe(12); 28 | act(() => { 29 | hook.result.current.event$.emit(2); 30 | }); 31 | expect(hook.result.current.count).toBe(26); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/hooks/src/useEventListener/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Default usage 3 | * desc: Click the button to preview. 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 点击按钮查看效果。 7 | */ 8 | 9 | import React, { useState, useRef } from 'react'; 10 | import { useEventListener } from 'ahooks'; 11 | 12 | export default () => { 13 | const [value, setValue] = useState(0); 14 | const ref = useRef(null); 15 | 16 | useEventListener( 17 | 'click', 18 | () => { 19 | setValue(value + 1); 20 | }, 21 | { target: ref }, 22 | ); 23 | 24 | return ( 25 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /packages/hooks/src/useEventListener/demo/demo2.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Listen keydown 3 | * desc: Press any key to preview. 4 | * 5 | * title.zh-CN: 监听 keydown 事件 6 | * desc.zh-CN: 按下键盘查看效果。 7 | */ 8 | 9 | import React, { useState } from 'react'; 10 | import { useEventListener } from 'ahooks'; 11 | 12 | export default () => { 13 | const [value, setValue] = useState(''); 14 | 15 | useEventListener('keydown', (ev) => { 16 | setValue(ev.code); 17 | }); 18 | 19 | return

Your press key is {value}

; 20 | }; 21 | -------------------------------------------------------------------------------- /packages/hooks/src/useEventListener/demo/demo3.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Listen to multiple events. 3 | * desc: Mouse hover or over the button to preview. 4 | * 5 | * title.zh-CN: 监听多个事件 6 | * desc.zh-CN: 鼠标移入移出按钮查看效果。 7 | */ 8 | 9 | import React, { useRef, useState } from 'react'; 10 | import { useEventListener } from 'ahooks'; 11 | 12 | export default () => { 13 | const ref = useRef(null); 14 | const [value, setValue] = useState(''); 15 | 16 | useEventListener( 17 | ['mouseenter', 'mouseleave'], 18 | (ev) => { 19 | setValue(ev.type); 20 | }, 21 | { target: ref }, 22 | ); 23 | 24 | return ( 25 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /packages/hooks/src/useEventTarget/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Basic usage 3 | * desc: Controlled input component,support reset. 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 受控的 input,支持 reset。 7 | */ 8 | 9 | import React from 'react'; 10 | import { useEventTarget } from 'ahooks'; 11 | 12 | export default () => { 13 | const [value, { reset, onChange }] = useEventTarget({ initialValue: 'this is initial value' }); 14 | 15 | return ( 16 |
17 | 18 | 21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/hooks/src/useEventTarget/demo/demo2.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Custom transformer function 3 | * desc: Controlled input component with number input only 4 | * 5 | * title.zh-CN: 自定义转换函数 6 | * desc.zh-CN: 只能输入数字的 input 组件 7 | */ 8 | 9 | import React from 'react'; 10 | import { useEventTarget } from 'ahooks'; 11 | 12 | export default () => { 13 | const [value, { onChange, reset }] = useEventTarget({ 14 | initialValue: '', 15 | transformer: (val: string) => val.replace(/[^\d]/g, ''), 16 | }); 17 | 18 | return ( 19 |
20 | 26 | 29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /packages/hooks/src/useExternal/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Default usage 3 | * desc: Load js file, such as [test-external-script.js](/useExternal/test-external-script.js) 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 加载 js 文件,例如引入 [test-external-script.js](/useExternal/test-external-script.js) 7 | */ 8 | 9 | import React from 'react'; 10 | import { useExternal } from 'ahooks'; 11 | 12 | export default () => { 13 | const status = useExternal('/useExternal/test-external-script.js', { 14 | js: { 15 | async: true, 16 | }, 17 | }); 18 | 19 | return ( 20 | <> 21 |

22 | Status: {status} 23 |

24 |

25 | Response: {status === 'ready' ? window.TEST_SCRIPT?.start() : '-'} 26 |

27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /packages/hooks/src/useFavicon/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Basic usage 3 | * desc: Set favicon 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 设置 favicon 7 | */ 8 | 9 | import React, { useState } from 'react'; 10 | import { useFavicon } from 'ahooks'; 11 | 12 | export const DEFAULT_FAVICON_URL = 'https://ahooks.js.org/simple-logo.svg'; 13 | 14 | export const GOOGLE_FAVICON_URL = 'https://www.google.com/favicon.ico'; 15 | 16 | export default () => { 17 | const [url, setUrl] = useState(DEFAULT_FAVICON_URL); 18 | 19 | useFavicon(url); 20 | 21 | return ( 22 | <> 23 |

24 | Current Favicon: {url} 25 |

26 | 34 | 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /packages/hooks/src/useFavicon/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useFavicon 7 | 8 | A hook that set the favicon of the page. 9 | 10 | ## Example 11 | 12 | ### Basic Usage 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | useFavicon(href: string); 20 | ``` 21 | 22 | ### Params 23 | 24 | | Params | Description | Type | Default | 25 | | ------ | -------------------------------------------- | -------- | ------- | 26 | | href | favicon URL, support `svg`/`png`/`ico`/`gif` | `string` | - | 27 | 28 | ## FAQ 29 | 30 | ### It doesn't work in Safari? 31 | 32 | Safari cannot set the favicon dynamically. 33 | 34 | > Apple intentionally do not want the ability to script favicons. See https://bugs.webkit.org/show_bug.cgi?id=95979#c2 35 | 36 | Related issues: [#2126](https://github.com/alibaba/hooks/issues/2126) 37 | -------------------------------------------------------------------------------- /packages/hooks/src/useFavicon/index.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | const ImgTypeMap = { 4 | SVG: 'image/svg+xml', 5 | ICO: 'image/x-icon', 6 | GIF: 'image/gif', 7 | PNG: 'image/png', 8 | }; 9 | 10 | type ImgTypes = keyof typeof ImgTypeMap; 11 | 12 | const useFavicon = (href: string) => { 13 | useEffect(() => { 14 | if (!href) return; 15 | 16 | const cutUrl = href.split('.'); 17 | const imgSuffix = cutUrl[cutUrl.length - 1].toLocaleUpperCase() as ImgTypes; 18 | 19 | const link: HTMLLinkElement = 20 | document.querySelector("link[rel*='icon']") || document.createElement('link'); 21 | 22 | link.type = ImgTypeMap[imgSuffix]; 23 | link.href = href; 24 | link.rel = 'shortcut icon'; 25 | 26 | document.getElementsByTagName('head')[0].appendChild(link); 27 | }, [href]); 28 | }; 29 | 30 | export default useFavicon; 31 | -------------------------------------------------------------------------------- /packages/hooks/src/useFavicon/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useFavicon 7 | 8 | 设置页面的 favicon。 9 | 10 | ## 代码演示 11 | 12 | ### 基础用法 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | useFavicon(href: string); 20 | ``` 21 | 22 | ### Params 23 | 24 | | 参数 | 说明 | 类型 | 默认值 | 25 | | ---- | ----------------------------------------------------- | -------- | ------ | 26 | | href | favicon 地址, 支持 `svg`/`png`/`ico`/`gif` 后缀的图片 | `string` | - | 27 | 28 | ## FAQ 29 | 30 | ### 在 Safari 中不工作? 31 | 32 | Safari 无法动态设置 favicon。 33 | 34 | > Apple intentionally do not want the ability to script favicons. See https://bugs.webkit.org/show_bug.cgi?id=95979#c2 35 | 36 | 相关 issue:[#2126](https://github.com/alibaba/hooks/issues/2126) 37 | -------------------------------------------------------------------------------- /packages/hooks/src/useFocusWithin/demo/demo2.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Pass in DOM element 3 | * desc: Pass in a function that returns the DOM element. 4 | * 5 | * title.zh-CN: 传入 DOM 元素 6 | * desc.zh-CN: 传入 function 并返回一个 dom 元素。 7 | */ 8 | 9 | import { useFocusWithin } from 'ahooks'; 10 | import React from 'react'; 11 | 12 | export default () => { 13 | const isFocusWithin = useFocusWithin(() => document.getElementById('focus-area')); 14 | 15 | return ( 16 |
17 |
25 | 28 | 31 |
32 |

isFocusWithin: {JSON.stringify(isFocusWithin)}

33 |
34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /packages/hooks/src/useFullscreen/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Default usage 3 | * desc: Use ref to set elements that need full screen 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 使用 ref 设置需要全屏的元素 7 | */ 8 | 9 | import React, { useRef } from 'react'; 10 | import { useFullscreen } from 'ahooks'; 11 | 12 | export default () => { 13 | const ref = useRef(null); 14 | const [isFullscreen, { enterFullscreen, exitFullscreen, toggleFullscreen }] = useFullscreen(ref); 15 | return ( 16 |
17 |
{isFullscreen ? 'Fullscreen' : 'Not fullscreen'}
18 |
19 | 22 | 25 | 28 |
29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /packages/hooks/src/useFullscreen/demo/demo2.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Image full screen 3 | * 4 | * title.zh-CN: 图片全屏 5 | */ 6 | 7 | import React from 'react'; 8 | import { useFullscreen } from 'ahooks'; 9 | import img from './react-hooks.jpg'; 10 | 11 | export default () => { 12 | const [, { enterFullscreen }] = useFullscreen(() => document.getElementById('fullscreen-img')); 13 | return ( 14 |
15 |
16 | 17 |
18 | 21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/hooks/src/useFullscreen/demo/demo3.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Page full screen 3 | * 4 | * title.zh-CN: 页面全屏 5 | */ 6 | 7 | import React, { useRef } from 'react'; 8 | import { useFullscreen } from 'ahooks'; 9 | 10 | export default () => { 11 | const ref = useRef(null); 12 | const [isFullscreen, { toggleFullscreen, enterFullscreen, exitFullscreen }] = useFullscreen(ref, { 13 | pageFullscreen: true, 14 | }); 15 | 16 | return ( 17 |
18 |
19 |
{isFullscreen ? 'Fullscreen' : 'Not fullscreen'}
20 | 23 | 26 | 29 |
30 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/hooks/src/useFullscreen/demo/react-hooks.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/hooks/37ffafebd972226d015535f70e4d8816f869e213/packages/hooks/src/useFullscreen/demo/react-hooks.jpg -------------------------------------------------------------------------------- /packages/hooks/src/useFusionTable/index.tsx: -------------------------------------------------------------------------------- 1 | import useAntdTable from '../useAntdTable'; 2 | import type { Data, Params, Service } from '../useAntdTable/types'; 3 | import { fieldAdapter, resultAdapter } from './fusionAdapter'; 4 | import type { FusionTableOptions, FusionTableResult } from './types'; 5 | 6 | const useFusionTable = ( 7 | service: Service, 8 | options: FusionTableOptions = {}, 9 | ): FusionTableResult => { 10 | const ret = useAntdTable(service, { 11 | ...options, 12 | form: options.field ? fieldAdapter(options.field) : undefined, 13 | }); 14 | 15 | return resultAdapter(ret); 16 | }; 17 | 18 | export default useFusionTable; 19 | -------------------------------------------------------------------------------- /packages/hooks/src/useGetState/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Open console to view logs 3 | * desc: The counter prints the value every 3 seconds 4 | * 5 | * title.zh-CN: 打开控制台查看输出 6 | * desc.zh-CN: 计数器每 3 秒打印一次值 7 | */ 8 | 9 | import React, { useEffect } from 'react'; 10 | import { useGetState } from 'ahooks'; 11 | 12 | export default () => { 13 | const [count, setCount, getCount] = useGetState(0); 14 | 15 | useEffect(() => { 16 | const interval = setInterval(() => { 17 | console.log('interval count', getCount()); 18 | }, 3000); 19 | 20 | return () => { 21 | clearInterval(interval); 22 | }; 23 | }, []); 24 | 25 | return ; 26 | }; 27 | -------------------------------------------------------------------------------- /packages/hooks/src/useGetState/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useGetState 7 | 8 | Add a getter method to the return value of `React.useState` to get the latest value 9 | 10 | ## Examples 11 | 12 | ### Default usage 13 | 14 | 15 | 16 | ## TypeScript definition 17 | 18 | ```typescript 19 | import { Dispatch, SetStateAction } from 'react'; 20 | type GetStateAction = () => S; 21 | 22 | function useGetState(initialState: S | (() => S)): [S, Dispatch>, GetStateAction]; 23 | function useGetState(): [S | undefined, Dispatch>, GetStateAction]; 24 | ``` 25 | 26 | ## API 27 | 28 | ```typescript 29 | const [state, setState, getState] = useGetState(initialState) 30 | ``` 31 | -------------------------------------------------------------------------------- /packages/hooks/src/useGetState/index.ts: -------------------------------------------------------------------------------- 1 | import type { Dispatch, SetStateAction } from 'react'; 2 | import { useState, useCallback } from 'react'; 3 | import useLatest from '../useLatest'; 4 | 5 | type GetStateAction = () => S; 6 | 7 | function useGetState( 8 | initialState: S | (() => S), 9 | ): [S, Dispatch>, GetStateAction]; 10 | function useGetState(): [ 11 | S | undefined, 12 | Dispatch>, 13 | GetStateAction, 14 | ]; 15 | function useGetState(initialState?: S) { 16 | const [state, setState] = useState(initialState); 17 | const stateRef = useLatest(state); 18 | 19 | const getState = useCallback(() => stateRef.current, []); 20 | 21 | return [state, setState, getState]; 22 | } 23 | 24 | export default useGetState; 25 | -------------------------------------------------------------------------------- /packages/hooks/src/useGetState/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useGetState 7 | 8 | 给 `React.useState` 增加了一个 getter 方法,以获取当前最新值。 9 | 10 | ## 代码演示 11 | 12 | ### 基础用法 13 | 14 | 15 | 16 | ## 类型定义 17 | 18 | ```typescript 19 | import { Dispatch, SetStateAction } from 'react'; 20 | type GetStateAction = () => S; 21 | 22 | function useGetState(initialState: S | (() => S)): [S, Dispatch>, GetStateAction]; 23 | function useGetState(): [S | undefined, Dispatch>, GetStateAction]; 24 | ``` 25 | 26 | ## API 27 | 28 | ```typescript 29 | const [state, setState, getState] = useGetState(initialState) 30 | ``` 31 | -------------------------------------------------------------------------------- /packages/hooks/src/useHistoryTravel/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Basic usage 3 | * desc: Redo and undo operations,click back and forward after input something. 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 撤销跟重做操作,输入内容后,点击 back 和 forward。 7 | */ 8 | 9 | import { useHistoryTravel } from 'ahooks'; 10 | import React from 'react'; 11 | 12 | export default () => { 13 | const { value, setValue, backLength, forwardLength, back, forward } = useHistoryTravel(); 14 | 15 | return ( 16 |
17 | setValue(e.target.value)} /> 18 | 21 | 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /packages/hooks/src/useHistoryTravel/demo/demo3.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Limit maximum history length 3 | * desc: Limit the maximum number of history records to avoid excessive memory consumption. 4 | * 5 | * title.zh-CN: 限制历史记录最大长度 6 | * desc.zh-CN: 限制最大历史记录数量,避免过度占用内存。 7 | */ 8 | 9 | import { useHistoryTravel } from 'ahooks'; 10 | import React from 'react'; 11 | 12 | export default () => { 13 | const maxLength = 3; 14 | const { value, setValue, backLength, forwardLength, back, forward } = useHistoryTravel( 15 | '', 16 | maxLength, 17 | ); 18 | 19 | return ( 20 |
21 |
maxLength: {maxLength}
22 |
backLength: {backLength}
23 |
forwardLength: {forwardLength}
24 | setValue(e.target.value)} /> 25 | 28 | 31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/hooks/src/useHover/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | // write your test cases here 2 | import React from 'react'; 3 | import { renderHook, act } from '@testing-library/react'; 4 | import { render, fireEvent } from '@testing-library/react'; 5 | import useHover from '../index'; 6 | 7 | describe('useHover', () => { 8 | it('should work', () => { 9 | const { getByText } = render(); 10 | let trigger = 0; 11 | const { result } = renderHook(() => 12 | useHover(getByText('Hover'), { 13 | onEnter: () => { 14 | trigger++; 15 | }, 16 | onLeave: () => { 17 | trigger++; 18 | }, 19 | }), 20 | ); 21 | 22 | expect(result.current).toBe(false); 23 | 24 | act(() => void fireEvent.mouseEnter(getByText('Hover'))); 25 | expect(result.current).toBe(true); 26 | expect(trigger).toBe(1); 27 | 28 | act(() => void fireEvent.mouseLeave(getByText('Hover'))); 29 | expect(result.current).toBe(false); 30 | expect(trigger).toBe(2); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /packages/hooks/src/useHover/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Basic usage 3 | * desc: Use ref to set element that needs monitoring. 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 使用 ref 设置需要监听的元素。 7 | */ 8 | 9 | import React, { useRef } from 'react'; 10 | import { useHover } from 'ahooks'; 11 | 12 | export default () => { 13 | const ref = useRef(null); 14 | const isHovering = useHover(ref); 15 | return
{isHovering ? 'hover' : 'leaveHover'}
; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/hooks/src/useHover/demo/demo2.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Pass in DOM element 3 | * desc: Pass in a function that returns the DOM element. 4 | * 5 | * title.zh-CN: 传入 DOM 元素 6 | * desc.zh-CN: 传入 function 并返回一个 dom 元素。 7 | */ 8 | 9 | import React from 'react'; 10 | import { useHover } from 'ahooks'; 11 | 12 | export default () => { 13 | const isHovering = useHover(() => document.getElementById('hover-div'), { 14 | onEnter: () => { 15 | console.log('onEnter'); 16 | }, 17 | onLeave: () => { 18 | console.log('onLeave'); 19 | }, 20 | onChange: isHover => { 21 | console.log('onChange', isHover); 22 | }, 23 | }); 24 | 25 | return
{isHovering ? 'hover' : 'leaveHover'}
; 26 | }; 27 | -------------------------------------------------------------------------------- /packages/hooks/src/useHover/index.ts: -------------------------------------------------------------------------------- 1 | import useBoolean from '../useBoolean'; 2 | import useEventListener from '../useEventListener'; 3 | import type { BasicTarget } from '../utils/domTarget'; 4 | 5 | export interface Options { 6 | onEnter?: () => void; 7 | onLeave?: () => void; 8 | onChange?: (isHovering: boolean) => void; 9 | } 10 | 11 | export default (target: BasicTarget, options?: Options): boolean => { 12 | const { onEnter, onLeave, onChange } = options || {}; 13 | 14 | const [state, { setTrue, setFalse }] = useBoolean(false); 15 | 16 | useEventListener( 17 | 'mouseenter', 18 | () => { 19 | onEnter?.(); 20 | setTrue(); 21 | onChange?.(true); 22 | }, 23 | { 24 | target, 25 | }, 26 | ); 27 | 28 | useEventListener( 29 | 'mouseleave', 30 | () => { 31 | onLeave?.(); 32 | setFalse(); 33 | onChange?.(false); 34 | }, 35 | { 36 | target, 37 | }, 38 | ); 39 | 40 | return state; 41 | }; 42 | -------------------------------------------------------------------------------- /packages/hooks/src/useInterval/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Basic usage 3 | * desc: Execute once per 1000ms. 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 每1000ms,执行一次 7 | */ 8 | 9 | import React, { useState } from 'react'; 10 | import { useInterval } from 'ahooks'; 11 | 12 | export default () => { 13 | const [count, setCount] = useState(0); 14 | 15 | useInterval(() => { 16 | setCount(count + 1); 17 | }, 1000); 18 | 19 | return
count: {count}
; 20 | }; 21 | -------------------------------------------------------------------------------- /packages/hooks/src/useInterval/index.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from 'react'; 2 | import useMemoizedFn from '../useMemoizedFn'; 3 | import { isNumber } from '../utils'; 4 | 5 | const useInterval = (fn: () => void, delay?: number, options: { immediate?: boolean } = {}) => { 6 | const timerCallback = useMemoizedFn(fn); 7 | const timerRef = useRef | null>(null); 8 | 9 | const clear = useCallback(() => { 10 | if (timerRef.current) { 11 | clearInterval(timerRef.current); 12 | } 13 | }, []); 14 | 15 | useEffect(() => { 16 | if (!isNumber(delay) || delay < 0) { 17 | return; 18 | } 19 | if (options.immediate) { 20 | timerCallback(); 21 | } 22 | timerRef.current = setInterval(timerCallback, delay); 23 | return clear; 24 | }, [delay, options.immediate]); 25 | 26 | return clear; 27 | }; 28 | 29 | export default useInterval; 30 | -------------------------------------------------------------------------------- /packages/hooks/src/useIsomorphicLayoutEffect/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react'; 2 | import useIsomorphicLayoutEffect from '../index'; 3 | 4 | describe('useIsomorphicLayoutEffect', () => { 5 | const callback = jest.fn(); 6 | const { result } = renderHook(() => useIsomorphicLayoutEffect(callback)); 7 | 8 | it('cheak return value', () => { 9 | expect(result.current).toBeUndefined(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /packages/hooks/src/useIsomorphicLayoutEffect/index.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useLayoutEffect } from 'react'; 2 | import isBrowser from '../utils/isBrowser'; 3 | 4 | const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : useEffect; 5 | 6 | export default useIsomorphicLayoutEffect; 7 | -------------------------------------------------------------------------------- /packages/hooks/src/useIsomorphicLayoutEffect/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useIsomorphicLayoutEffect 7 | 8 | 在 SSR 模式下,使用 useLayoutEffect 时,会出现以下警告 9 | 10 | > ⚠️ 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://fb.me/react-uselayouteffect-ssr for common fixes. 11 | 12 | 为了避免该警告,可以使用 useIsomorphicLayoutEffect 代替 useLayoutEffect。 13 | 14 | useIsomorphicLayoutEffect 源码如下: 15 | 16 | ```js 17 | const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : useEffect; 18 | ``` 19 | 20 | 在非浏览器环境返回 useEffect,在浏览器环境返回 useLayoutEffect。 21 | 22 | 更多信息可以参考 [useLayoutEffect and SSR](https://medium.com/@alexandereardon/uselayouteffect-and-ssr-192986cdcf7a) 23 | -------------------------------------------------------------------------------- /packages/hooks/src/useKeyPress/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Basic usage 3 | * desc: Supported keyCode and alias in keyboard events, pressing ArrowUp or ArrowDown to show effect. 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 支持键盘事件中的 keyCode 和别名,请按 ArrowUp 或 ArrowDown 键进行演示。 7 | */ 8 | 9 | import React, { useState } from 'react'; 10 | import { useKeyPress } from 'ahooks'; 11 | 12 | export default () => { 13 | const [counter, setCounter] = useState(0); 14 | 15 | useKeyPress('uparrow', () => { 16 | setCounter((s) => s + 1); 17 | }); 18 | 19 | // keyCode value for ArrowDown 20 | useKeyPress(40, () => { 21 | setCounter((s) => s - 1); 22 | }); 23 | 24 | return ( 25 |
26 |

Try pressing the following:

27 |
1. Press ArrowUp by key to increase
28 |
2. Press ArrowDown by keyCode to decrease
29 |
30 | counter: {counter} 31 |
32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /packages/hooks/src/useKeyPress/demo/demo2.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Use key aliases 3 | * desc: Support using key aliases. Please refer to the [document](#remarks) below. 4 | * 5 | * title.zh-CN: 使用别名 6 | * desc.zh-CN: 支持使用别名,更多内容请[查看备注](#remarks)。 7 | */ 8 | 9 | import React, { useState } from 'react'; 10 | import { useKeyPress } from 'ahooks'; 11 | 12 | export default () => { 13 | const [counter, setCounter] = useState(0); 14 | 15 | useKeyPress('leftarrow', () => { 16 | setCounter((s) => s - 1); 17 | }); 18 | 19 | useKeyPress('rightarrow', () => { 20 | setCounter((s) => s + 1); 21 | }); 22 | 23 | return ( 24 |
25 |

Try pressing the following:

26 |
1. Press ArrowLeft to decrease
27 |
2. Press ArrowRight to increase
28 |
29 | counter: {counter} 30 |
31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/hooks/src/useKeyPress/demo/demo3.tsx: -------------------------------------------------------------------------------- 1 | import { useKeyPress } from 'ahooks'; 2 | import React, { useState } from 'react'; 3 | 4 | export default () => { 5 | const [num, setNum] = useState(); 6 | const [key, setKey] = useState(); 7 | 8 | const filterKey = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; 9 | useKeyPress(filterKey, (event) => { 10 | setNum(event.key); 11 | }); 12 | 13 | // a s d f, Backspace, 8 14 | useKeyPress([65, 83, 68, 70, 8, '8'], (event) => { 15 | setKey(event.key); 16 | }); 17 | 18 | return ( 19 |
20 |

Try pressing the following:

21 |
22 | 1. Number key [0-9]: {num} 23 |
24 |
25 | 2. Press key [a, s, d, f, Backspace, 8]: {key} 26 |
27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /packages/hooks/src/useKeyPress/demo/demo4.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Advanced 3 | * desc: Supports receiving a Boolean callback function to handle preprocessing operations. 4 | * 5 | * title.zh-CN: 进阶使用 6 | * desc.zh-CN: 支持接收一个返回 boolean 的回调函数,自己处理逻辑。 7 | */ 8 | 9 | import React, { useState } from 'react'; 10 | import { useKeyPress } from 'ahooks'; 11 | 12 | export default () => { 13 | const [key, setKey] = useState(); 14 | const filterKey = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; 15 | useKeyPress( 16 | (event) => !filterKey.includes(event.key), 17 | (event) => { 18 | if (event.type === 'keyup') { 19 | setKey(event.key); 20 | } 21 | }, 22 | { 23 | events: ['keydown', 'keyup'], 24 | }, 25 | ); 26 | 27 | return ( 28 |
29 | Pressing key except number key:{key} 30 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/hooks/src/useLatest/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react'; 2 | import useLatest from '../index'; 3 | 4 | const setUp = (val) => renderHook((state) => useLatest(state), { initialProps: val }); 5 | 6 | describe('useLatest', () => { 7 | it('useLatest with basic variable should work', async () => { 8 | const { result, rerender } = setUp(0); 9 | 10 | rerender(1); 11 | expect(result.current.current).toBe(1); 12 | 13 | rerender(2); 14 | expect(result.current.current).toBe(2); 15 | 16 | rerender(3); 17 | expect(result.current.current).toBe(3); 18 | }); 19 | 20 | it('useLatest with reference variable should work', async () => { 21 | const val1 = {}; 22 | const { result, rerender } = setUp(val1); 23 | 24 | expect(result.current.current).toBe(val1); 25 | 26 | const val2 = []; 27 | rerender(val2); 28 | expect(result.current.current).toBe(val2); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/hooks/src/useLatest/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Basic usage 3 | * desc: useLatest always returns the latest value 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: useLatest 返回的永远是最新值 7 | */ 8 | 9 | import React, { useState, useEffect } from 'react'; 10 | import { useLatest } from 'ahooks'; 11 | 12 | export default () => { 13 | const [count, setCount] = useState(0); 14 | const [count2, setCount2] = useState(0); 15 | 16 | const latestCountRef = useLatest(count); 17 | 18 | useEffect(() => { 19 | const interval = setInterval(() => { 20 | setCount(latestCountRef.current + 1); 21 | }, 1000); 22 | return () => clearInterval(interval); 23 | }, []); 24 | 25 | useEffect(() => { 26 | const interval = setInterval(() => { 27 | setCount2(count2 + 1); 28 | }, 1000); 29 | return () => clearInterval(interval); 30 | }, []); 31 | 32 | return ( 33 | <> 34 |

count(useLatest): {count}

35 |

count(default): {count2}

36 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /packages/hooks/src/useLatest/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useLatest 7 | 8 | A Hook that returns the latest value, effectively avoiding the closure problem. 9 | 10 | ## Examples 11 | 12 | ### Basic usage 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | const latestValueRef = useLatest(value: T): MutableRefObject; 20 | ``` 21 | -------------------------------------------------------------------------------- /packages/hooks/src/useLatest/index.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | function useLatest(value: T) { 4 | const ref = useRef(value); 5 | ref.current = value; 6 | 7 | return ref; 8 | } 9 | 10 | export default useLatest; 11 | -------------------------------------------------------------------------------- /packages/hooks/src/useLatest/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useLatest 7 | 8 | 返回当前最新值的 Hook,可以避免闭包问题。 9 | 10 | ## 代码演示 11 | 12 | ### 基础用法 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | const latestValueRef = useLatest(value: T): MutableRefObject; 20 | ``` 21 | -------------------------------------------------------------------------------- /packages/hooks/src/useLocalStorageState/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Store state into localStorage 3 | * desc: Refresh this page and you will get the state from localStorage. 4 | * 5 | * title.zh-CN: 将 state 存储在 localStorage 中 6 | * desc.zh-CN: 刷新页面后,可以看到输入框中的内容被从 localStorage 中恢复了。 7 | */ 8 | 9 | import React from 'react'; 10 | import { useLocalStorageState } from 'ahooks'; 11 | 12 | export default function () { 13 | const [message, setMessage] = useLocalStorageState( 14 | 'use-local-storage-state-demo1', 15 | { 16 | defaultValue: 'Hello~', 17 | }, 18 | ); 19 | 20 | return ( 21 | <> 22 | setMessage(e.target.value)} 26 | /> 27 | 30 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /packages/hooks/src/useLocalStorageState/demo/demo2.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Store complex types such as array or object 3 | * desc: useLocalStorageState will do the serialization and deserialization work automatically. 4 | * 5 | * title.zh-CN: 存储数组或对象等复杂类型 6 | * desc.zh-CN: useLocalStorageState 会自动处理序列化和反序列化的操作。 7 | */ 8 | 9 | import React from 'react'; 10 | import { useLocalStorageState } from 'ahooks'; 11 | 12 | const defaultArray = ['a', 'e', 'i', 'o', 'u']; 13 | 14 | export default function () { 15 | const [value, setValue] = useLocalStorageState('use-local-storage-state-demo2', { 16 | defaultValue: defaultArray, 17 | }); 18 | 19 | return ( 20 | <> 21 |

{value?.join('-')}

22 | 29 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /packages/hooks/src/useLocalStorageState/index.ts: -------------------------------------------------------------------------------- 1 | import { createUseStorageState } from '../createUseStorageState'; 2 | import isBrowser from '../utils/isBrowser'; 3 | 4 | const useLocalStorageState = createUseStorageState(() => (isBrowser ? localStorage : undefined)); 5 | 6 | export default useLocalStorageState; 7 | -------------------------------------------------------------------------------- /packages/hooks/src/useLockFn/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Prevent duplicated submits 3 | * desc: Before the `submit` function finishes, the other click actions will be ignored. 4 | * 5 | * title.zh-CN: 防止重复提交 6 | * desc.zh-CN: 在 `submit` 函数执行完成前,其余的点击动作都会被忽略。 7 | */ 8 | 9 | import { useLockFn } from 'ahooks'; 10 | import { message } from 'antd'; 11 | import React, { useState } from 'react'; 12 | 13 | function mockApiRequest() { 14 | return new Promise((resolve) => { 15 | setTimeout(() => { 16 | resolve(); 17 | }, 2000); 18 | }); 19 | } 20 | 21 | export default () => { 22 | const [count, setCount] = useState(0); 23 | 24 | const submit = useLockFn(async () => { 25 | message.info('Start to submit'); 26 | await mockApiRequest(); 27 | setCount((val) => val + 1); 28 | message.success('Submit finished'); 29 | }); 30 | 31 | return ( 32 | <> 33 |

Submit count: {count}

34 | 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /packages/hooks/src/useLockFn/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useLockFn 7 | 8 | Add lock to an async function to prevent parallel executions. 9 | 10 | ## Examples 11 | 12 | ### Basic usage 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | function useLockFn

( 20 | fn: (...args: P) => Promise 21 | ): fn: (...args: P) => Promise; 22 | ``` 23 | 24 | ### Result 25 | 26 | | Property | Description | Type | 27 | | -------- | ---------------------------- | ---------------------------------- | 28 | | fn | The async function with lock | `(...args: any[]) => Promise` | 29 | 30 | ### Params 31 | 32 | | Property | Description | Type | Default | 33 | | -------- | ----------------- | ---------------------------------- | ------- | 34 | | fn | An async function | `(...args: any[]) => Promise` | - | 35 | -------------------------------------------------------------------------------- /packages/hooks/src/useLockFn/index.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useCallback } from 'react'; 2 | 3 | function useLockFn

(fn: (...args: P) => Promise) { 4 | const lockRef = useRef(false); 5 | 6 | return useCallback( 7 | async (...args: P) => { 8 | if (lockRef.current) return; 9 | lockRef.current = true; 10 | try { 11 | const ret = await fn(...args); 12 | return ret; 13 | } catch (e) { 14 | throw e; 15 | } finally { 16 | lockRef.current = false; 17 | } 18 | }, 19 | [fn], 20 | ); 21 | } 22 | 23 | export default useLockFn; 24 | -------------------------------------------------------------------------------- /packages/hooks/src/useLockFn/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useLockFn 7 | 8 | 用于给一个异步函数增加竞态锁,防止并发执行。 9 | 10 | ## 代码演示 11 | 12 | ### 基础用法 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | function useLockFn

( 20 | fn: (...args: P) => Promise 21 | ): fn: (...args: P) => Promise; 22 | ``` 23 | 24 | ### Result 25 | 26 | | 参数 | 说明 | 类型 | 27 | | ---- | ------------------ | ---------------------------------- | 28 | | fn | 增加了竞态锁的函数 | `(...args: any[]) => Promise` | 29 | 30 | ### Params 31 | 32 | | 参数 | 说明 | 类型 | 默认值 | 33 | | ---- | -------------------- | ---------------------------------- | ------ | 34 | | fn | 需要增加竞态锁的函数 | `(...args: any[]) => Promise` | - | 35 | -------------------------------------------------------------------------------- /packages/hooks/src/useLongPress/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Default usage 3 | * desc: Please keep pressing button to show effects. 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 请长按按钮查看效果。 7 | */ 8 | 9 | import React, { useState, useRef } from 'react'; 10 | import { useLongPress } from 'ahooks'; 11 | 12 | export default () => { 13 | const [counter, setCounter] = useState(0); 14 | const ref = useRef(null); 15 | 16 | useLongPress(() => setCounter((s) => s + 1), ref); 17 | 18 | return ( 19 |

20 | 23 |

counter: {counter}

24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /packages/hooks/src/useLongPress/demo/demo2.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import { useLongPress } from 'ahooks'; 3 | 4 | export default () => { 5 | const [pressCounter, setPressCounter] = useState(0); 6 | const [clickCounter, setClickCounter] = useState(0); 7 | 8 | const ref = useRef(null); 9 | 10 | useLongPress(() => setPressCounter((s) => s + 1), ref, { 11 | onClick: () => setClickCounter((s) => s + 1), 12 | }); 13 | 14 | return ( 15 |
16 | 19 |

pressCounter: {pressCounter}

20 |

clickCounter: {clickCounter}

21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/hooks/src/useLongPress/demo/demo3.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Basic usage 3 | * desc: After the movement threshold is exceeded, the long press event will not be triggered 4 | * 5 | * title.zh-CN: 超出移动阈值 6 | * desc.zh-CN: 超出移动阈值之后,长按事件将不会触发 7 | */ 8 | import React, { useRef, useState } from 'react'; 9 | import { useLongPress } from 'ahooks'; 10 | 11 | export default () => { 12 | const [pressCounter, setPressCounter] = useState(0); 13 | 14 | const ref = useRef(null); 15 | 16 | useLongPress(() => setPressCounter((s) => s + 1), ref, { 17 | moveThreshold: { x: 30 }, 18 | }); 19 | 20 | return ( 21 |
22 | 25 |

counter: {pressCounter}

26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /packages/hooks/src/useMap/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useMap } from 'ahooks'; 3 | 4 | export default () => { 5 | const [map, { set, setAll, remove, reset, get }] = useMap([ 6 | ['msg', 'hello world'], 7 | [123, 'number type'], 8 | ]); 9 | 10 | return ( 11 |
12 | 15 | 22 | 25 | 28 |
29 |
{JSON.stringify(Array.from(map), null, 2)}
30 |
31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/hooks/src/useMemoizedFn/index.ts: -------------------------------------------------------------------------------- 1 | import { useMemo, useRef } from 'react'; 2 | import { isFunction } from '../utils'; 3 | import isDev from '../utils/isDev'; 4 | 5 | type noop = (this: any, ...args: any[]) => any; 6 | 7 | type PickFunction = ( 8 | this: ThisParameterType, 9 | ...args: Parameters 10 | ) => ReturnType; 11 | 12 | function useMemoizedFn(fn: T) { 13 | if (isDev) { 14 | if (!isFunction(fn)) { 15 | console.error(`useMemoizedFn expected parameter is a function, got ${typeof fn}`); 16 | } 17 | } 18 | 19 | const fnRef = useRef(fn); 20 | 21 | // why not write `fnRef.current = fn`? 22 | // https://github.com/alibaba/hooks/issues/728 23 | fnRef.current = useMemo(() => fn, [fn]); 24 | 25 | const memoizedFn = useRef>(); 26 | if (!memoizedFn.current) { 27 | memoizedFn.current = function (this, ...args) { 28 | return fnRef.current.apply(this, args); 29 | }; 30 | } 31 | 32 | return memoizedFn.current as PickFunction; 33 | } 34 | 35 | export default useMemoizedFn; 36 | -------------------------------------------------------------------------------- /packages/hooks/src/useMount/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react'; 2 | import useMount from '../index'; 3 | 4 | describe('useMount', () => { 5 | it('test mount', async () => { 6 | const fn = jest.fn(); 7 | const hook = renderHook(() => useMount(fn)); 8 | expect(fn).toBeCalledTimes(1); 9 | hook.rerender(); 10 | expect(fn).toBeCalledTimes(1); 11 | hook.unmount(); 12 | expect(fn).toBeCalledTimes(1); 13 | 14 | renderHook(() => useMount(fn)).unmount(); 15 | expect(fn).toBeCalledTimes(2); 16 | }); 17 | 18 | // it('should output error when fn is not a function', () => { 19 | // const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); 20 | // renderHook(() => useMount(1 as any)); 21 | // expect(errSpy).toBeCalledWith( 22 | // 'useMount: parameter `fn` expected to be a function, but got "number".', 23 | // ); 24 | // errSpy.mockRestore(); 25 | // }); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/hooks/src/useMount/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Basic usage 3 | * desc: The function is called right after the component is mounted. 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 在组件首次渲染时,执行方法。 7 | */ 8 | 9 | import { useMount, useBoolean } from 'ahooks'; 10 | import { message } from 'antd'; 11 | import React from 'react'; 12 | 13 | const MyComponent = () => { 14 | useMount(() => { 15 | message.info('mount'); 16 | }); 17 | 18 | return
Hello World
; 19 | }; 20 | 21 | export default () => { 22 | const [state, { toggle }] = useBoolean(false); 23 | 24 | return ( 25 | <> 26 | 29 | {state && } 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/hooks/src/useMount/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useMount 7 | 8 | A hook that executes a function after the component is mounted. 9 | 10 | ## Examples 11 | 12 | ### Default Usage 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | useMount(fn: () => void); 20 | ``` 21 | 22 | ### Params 23 | 24 | | Property | Description | Type | Default | 25 | | -------- | --------------------------- | ------------ | ------- | 26 | | fn | The function to be executed | `() => void` | - | 27 | -------------------------------------------------------------------------------- /packages/hooks/src/useMount/index.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { isFunction } from '../utils'; 3 | import isDev from '../utils/isDev'; 4 | 5 | const useMount = (fn: () => void) => { 6 | if (isDev) { 7 | if (!isFunction(fn)) { 8 | console.error( 9 | `useMount: parameter \`fn\` expected to be a function, but got "${typeof fn}".`, 10 | ); 11 | } 12 | } 13 | 14 | useEffect(() => { 15 | fn?.(); 16 | }, []); 17 | }; 18 | 19 | export default useMount; 20 | -------------------------------------------------------------------------------- /packages/hooks/src/useMount/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useMount 7 | 8 | 只在组件初始化时执行的 Hook。 9 | 10 | ## 代码演示 11 | 12 | ### 基础用法 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | useMount(fn: () => void); 20 | ``` 21 | 22 | ### 参数 23 | 24 | | 参数 | 说明 | 类型 | 默认值 | 25 | | ---- | ------------------ | ------------ | ------ | 26 | | fn | 初始化时执行的函数 | `() => void` | - | 27 | -------------------------------------------------------------------------------- /packages/hooks/src/useMouse/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Default usage 3 | * desc: Tracking cursor position. 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 获取鼠标位置。 7 | */ 8 | 9 | import { useMouse } from 'ahooks'; 10 | import React from 'react'; 11 | 12 | export default () => { 13 | const mouse = useMouse(); 14 | 15 | return ( 16 |
17 |

18 | Client - x: {mouse.clientX}, y: {mouse.clientY} 19 |

20 |

21 | Page - x: {mouse.pageX}, y: {mouse.pageY} 22 |

23 |

24 | Screen - x: {mouse.screenX}, y: {mouse.screenY} 25 |

26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /packages/hooks/src/useMutationObserver/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Basic usage 3 | * 4 | * title.zh-CN: 基础用法 5 | */ 6 | 7 | import { useMutationObserver } from 'ahooks'; 8 | import React, { useRef, useState } from 'react'; 9 | 10 | const App: React.FC = () => { 11 | const [width, setWidth] = useState(200); 12 | const [count, setCount] = useState(0); 13 | 14 | const ref = useRef(null); 15 | 16 | useMutationObserver( 17 | (mutationsList) => { 18 | mutationsList.forEach(() => setCount((c) => c + 1)); 19 | }, 20 | ref, 21 | { attributes: true }, 22 | ); 23 | 24 | return ( 25 |
26 |
27 | current width:{width} 28 |
29 | 30 |

Mutation count {count}

31 |
32 | ); 33 | }; 34 | 35 | export default App; 36 | -------------------------------------------------------------------------------- /packages/hooks/src/useMutationObserver/index.ts: -------------------------------------------------------------------------------- 1 | import { getTargetElement } from '../utils/domTarget'; 2 | import type { BasicTarget } from '../utils/domTarget'; 3 | import useDeepCompareEffectWithTarget from '../utils/useDeepCompareWithTarget'; 4 | import useLatest from '../useLatest'; 5 | 6 | const useMutationObserver = ( 7 | callback: MutationCallback, 8 | target: BasicTarget, 9 | options: MutationObserverInit = {}, 10 | ): void => { 11 | const callbackRef = useLatest(callback); 12 | 13 | useDeepCompareEffectWithTarget( 14 | () => { 15 | const element = getTargetElement(target); 16 | if (!element) { 17 | return; 18 | } 19 | const observer = new MutationObserver(callbackRef.current); 20 | observer.observe(element, options); 21 | return () => { 22 | observer?.disconnect(); 23 | }; 24 | }, 25 | [options], 26 | target, 27 | ); 28 | }; 29 | 30 | export default useMutationObserver; 31 | -------------------------------------------------------------------------------- /packages/hooks/src/useNetwork/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import useNetwork from '../index'; 2 | import { renderHook, act } from '@testing-library/react'; 3 | 4 | describe('useNetwork', () => { 5 | it('toggle network state', () => { 6 | const { result } = renderHook(() => useNetwork()); 7 | expect(result.current.online).toBeTruthy(); 8 | act(() => { 9 | window.dispatchEvent(new Event('offline')); 10 | }); 11 | expect(result.current.online).toBeFalsy(); 12 | act(() => { 13 | window.dispatchEvent(new Event('online')); 14 | }); 15 | expect(result.current.online).toBeTruthy(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/hooks/src/useNetwork/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Basic usage 3 | * desc: Return network status 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 返回网络状态信息 7 | */ 8 | 9 | import React from 'react'; 10 | import { useNetwork } from 'ahooks'; 11 | 12 | export default () => { 13 | const networkState = useNetwork(); 14 | 15 | return ( 16 |
17 |
Network information:
18 |
{JSON.stringify(networkState, null, 2)}
19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/hooks/src/usePagination/types.ts: -------------------------------------------------------------------------------- 1 | import type { Options, Result } from '../useRequest/src/types'; 2 | 3 | export type Data = { total: number; list: any[] }; 4 | 5 | export type Params = [{ current: number; pageSize: number; [key: string]: any }, ...any[]]; 6 | 7 | export type Service = ( 8 | ...args: TParams 9 | ) => Promise; 10 | 11 | export interface PaginationResult 12 | extends Result { 13 | pagination: { 14 | current: number; 15 | pageSize: number; 16 | total: number; 17 | totalPage: number; 18 | onChange: (current: number, pageSize: number) => void; 19 | changeCurrent: (current: number) => void; 20 | changePageSize: (pageSize: number) => void; 21 | }; 22 | } 23 | 24 | export interface PaginationOptions 25 | extends Options { 26 | defaultPageSize?: number; 27 | defaultCurrent?: number; 28 | } 29 | -------------------------------------------------------------------------------- /packages/hooks/src/usePrevious/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Basic usage 3 | * desc: Record the previous value. 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 记录上次的 count 值 7 | */ 8 | 9 | import { usePrevious } from 'ahooks'; 10 | import React, { useState } from 'react'; 11 | 12 | export default () => { 13 | const [count, setCount] = useState(0); 14 | const previous = usePrevious(count); 15 | return ( 16 | <> 17 |
counter current value: {count}
18 |
counter previous value: {previous}
19 | 22 | 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /packages/hooks/src/usePrevious/index.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | export type ShouldUpdateFunc = (prev: T | undefined, next: T) => boolean; 4 | 5 | const defaultShouldUpdate = (a?: T, b?: T) => !Object.is(a, b); 6 | 7 | function usePrevious( 8 | state: T, 9 | shouldUpdate: ShouldUpdateFunc = defaultShouldUpdate, 10 | ): T | undefined { 11 | const prevRef = useRef(); 12 | const curRef = useRef(); 13 | 14 | if (shouldUpdate(curRef.current, state)) { 15 | prevRef.current = curRef.current; 16 | curRef.current = state; 17 | } 18 | 19 | return prevRef.current; 20 | } 21 | 22 | export default usePrevious; 23 | -------------------------------------------------------------------------------- /packages/hooks/src/useRafInterval/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Basic usage 3 | * desc: Execute once per 1000ms. 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 每1000ms,执行一次 7 | */ 8 | 9 | import React, { useState } from 'react'; 10 | import { useRafInterval } from 'ahooks'; 11 | 12 | export default () => { 13 | const [count, setCount] = useState(0); 14 | 15 | useRafInterval(() => { 16 | setCount(count + 1); 17 | }, 1000); 18 | 19 | return
count: {count}
; 20 | }; 21 | -------------------------------------------------------------------------------- /packages/hooks/src/useRafState/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '@testing-library/react'; 2 | import useRafState from '../index'; 3 | 4 | describe('useRafState', () => { 5 | it('should work', () => { 6 | const mockRaf = jest 7 | .spyOn(window, 'requestAnimationFrame') 8 | .mockImplementation((cb: FrameRequestCallback) => { 9 | cb(0); 10 | return 0; 11 | }); 12 | const { result } = renderHook(() => useRafState(0)); 13 | const setRafState = result.current[1]; 14 | expect(result.current[0]).toBe(0); 15 | 16 | act(() => { 17 | setRafState(1); 18 | }); 19 | expect(result.current[0]).toBe(1); 20 | mockRaf.mockRestore(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/hooks/src/useRafState/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Default usage 3 | * 4 | * title.zh-CN: 基础用法 5 | */ 6 | 7 | import { useRafState } from 'ahooks'; 8 | import React, { useEffect } from 'react'; 9 | 10 | export default () => { 11 | const [state, setState] = useRafState({ 12 | width: 0, 13 | height: 0, 14 | }); 15 | 16 | useEffect(() => { 17 | const onResize = () => { 18 | setState({ 19 | width: document.documentElement.clientWidth, 20 | height: document.documentElement.clientHeight, 21 | }); 22 | }; 23 | onResize(); 24 | 25 | window.addEventListener('resize', onResize); 26 | 27 | return () => { 28 | window.removeEventListener('resize', onResize); 29 | }; 30 | }, []); 31 | 32 | return ( 33 |
34 |

Try to resize the window

35 | current: {JSON.stringify(state)} 36 |
37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /packages/hooks/src/useRafState/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useRafState 7 | 8 | Update the state in [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) callback, generally used for performance optimization. 9 | 10 | ## Examples 11 | 12 | ### Default usage 13 | 14 | 15 | 16 | ### API 17 | 18 | Same as `React.useState`. 19 | -------------------------------------------------------------------------------- /packages/hooks/src/useRafState/index.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef, useState } from 'react'; 2 | import type { Dispatch, SetStateAction } from 'react'; 3 | import useUnmount from '../useUnmount'; 4 | 5 | function useRafState(initialState: S | (() => S)): [S, Dispatch>]; 6 | function useRafState(): [S | undefined, Dispatch>]; 7 | 8 | function useRafState(initialState?: S | (() => S)) { 9 | const ref = useRef(0); 10 | const [state, setState] = useState(initialState); 11 | 12 | const setRafState = useCallback((value: S | ((prevState: S) => S)) => { 13 | cancelAnimationFrame(ref.current); 14 | 15 | ref.current = requestAnimationFrame(() => { 16 | setState(value); 17 | }); 18 | }, []); 19 | 20 | useUnmount(() => { 21 | cancelAnimationFrame(ref.current); 22 | }); 23 | 24 | return [state, setRafState] as const; 25 | } 26 | 27 | export default useRafState; 28 | -------------------------------------------------------------------------------- /packages/hooks/src/useRafState/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useRafState 7 | 8 | 只在 [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) callback 时更新 state,一般用于性能优化。 9 | 10 | ## 代码演示 11 | 12 | ### 基础用法 13 | 14 | 15 | 16 | ### API 17 | 18 | 与 `React.useState` 一致 19 | -------------------------------------------------------------------------------- /packages/hooks/src/useRafTimeout/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Basic usage 3 | * desc: Execute after 2000ms. 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 在 2000ms 后执行。 7 | */ 8 | 9 | import React, { useState } from 'react'; 10 | import { useRafTimeout } from 'ahooks'; 11 | 12 | export default () => { 13 | const [count, setCount] = useState(0); 14 | 15 | useRafTimeout(() => { 16 | setCount(count + 1); 17 | }, 2000); 18 | 19 | return
count: {count}
; 20 | }; 21 | -------------------------------------------------------------------------------- /packages/hooks/src/useReactive/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useReactive } from 'ahooks'; 3 | 4 | export default () => { 5 | const state = useReactive({ 6 | count: 0, 7 | inputVal: '', 8 | obj: { 9 | value: '', 10 | }, 11 | }); 12 | 13 | return ( 14 |
15 |

state.count:{state.count}

16 | 17 | 20 | 21 | 22 |

state.inputVal: {state.inputVal}

23 | (state.inputVal = e.target.value)} /> 24 | 25 |

state.obj.value: {state.obj.value}

26 | (state.obj.value = e.target.value)} /> 27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /packages/hooks/src/useReactive/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | ## useReactive 7 | 8 | 提供一种数据响应式的操作体验,定义数据状态不需要写`useState`,直接修改属性即可刷新视图。 9 | 10 | ## 代码演示 11 | 12 | ### 基本用法 13 | 14 | 15 | 16 | ### 数组操作 17 | 18 | 19 | 20 | ### 计算属性 21 | 22 | 23 | 24 | ### 注意 25 | 26 | 27 | 28 | ## API 29 | 30 | ```js 31 | const state = useReactive(initialState: Record); 32 | ``` 33 | 34 | ## 参数 35 | 36 | | 参数 | 说明 | 类型 | 默认值 | 37 | | ------------ | -------------- | --------------------- | ------ | 38 | | initialState | 当前的数据对象 | `Record` | - | 39 | 40 | ## FAQ 41 | 42 | ### `useReactive` 和 `Map`、`Set` 一起使用时报错或无效? 43 | 44 | `useReactive` 目前不兼容 `Map`、`Set`。 45 | 46 | 相关 issues:[#2239](https://github.com/alibaba/hooks/discussions/2239) 47 | -------------------------------------------------------------------------------- /packages/hooks/src/useRequest/doc/basic/demo/default.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Read username 3 | * 4 | * title.zh-CN: 读取用户名称 5 | */ 6 | 7 | import { useRequest } from 'ahooks'; 8 | import Mock from 'mockjs'; 9 | import React from 'react'; 10 | 11 | function getUsername(): Promise { 12 | return new Promise((resolve, reject) => { 13 | setTimeout(() => { 14 | if (Math.random() > 0.5) { 15 | resolve(Mock.mock('@name')); 16 | } else { 17 | reject(new Error('Failed to get username')); 18 | } 19 | }, 1000); 20 | }); 21 | } 22 | 23 | export default () => { 24 | const { data, error, loading } = useRequest(getUsername); 25 | 26 | if (error) { 27 | return
{error.message}
; 28 | } 29 | if (loading) { 30 | return
loading...
; 31 | } 32 | return
Username: {data}
; 33 | }; 34 | -------------------------------------------------------------------------------- /packages/hooks/src/useRequest/doc/basic/demo/refresh.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Refresh username 3 | * 4 | * title.zh-CN: 刷新用户名称 5 | */ 6 | 7 | import { useRequest } from 'ahooks'; 8 | import Mock from 'mockjs'; 9 | import React, { useEffect } from 'react'; 10 | 11 | function getUsername(id: number): Promise { 12 | console.log('use-request-refresh-id', id); 13 | return new Promise((resolve) => { 14 | setTimeout(() => { 15 | resolve(Mock.mock('@name')); 16 | }, 1000); 17 | }); 18 | } 19 | 20 | export default () => { 21 | const { data, loading, run, refresh } = useRequest((id: number) => getUsername(id), { 22 | manual: true, 23 | }); 24 | 25 | useEffect(() => { 26 | run(1); 27 | }, []); 28 | 29 | if (loading) { 30 | return
loading...
; 31 | } 32 | return ( 33 |
34 |

Username: {data}

35 | 38 |
39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /packages/hooks/src/useRequest/doc/debounce/demo/debounce.tsx: -------------------------------------------------------------------------------- 1 | import { useRequest } from 'ahooks'; 2 | import Mock from 'mockjs'; 3 | import React from 'react'; 4 | 5 | async function getEmail(search?: string): Promise { 6 | console.log('debounce getEmail', search); 7 | return new Promise((resolve) => { 8 | setTimeout(() => { 9 | resolve(Mock.mock({ 'data|5': ['@email'] }).data); 10 | }, 300); 11 | }); 12 | } 13 | 14 | export default () => { 15 | const { data, loading, run } = useRequest(getEmail, { 16 | debounceWait: 1000, 17 | manual: true, 18 | }); 19 | 20 | return ( 21 |
22 | run(e.target.value)} /> 23 | {loading ? ( 24 |

loading

25 | ) : ( 26 |
    27 | {data?.map((i) => ( 28 |
  • {i}
  • 29 | ))} 30 |
31 | )} 32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /packages/hooks/src/useRequest/doc/index/demo/default.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Read username 3 | * 4 | * title.zh-CN: 读取用户名称 5 | */ 6 | 7 | import { useRequest } from 'ahooks'; 8 | import Mock from 'mockjs'; 9 | import React from 'react'; 10 | 11 | function getUsername(): Promise { 12 | return new Promise((resolve) => { 13 | setTimeout(() => { 14 | resolve(Mock.mock('@name')); 15 | }, 1000); 16 | }); 17 | } 18 | 19 | export default () => { 20 | const { data, error, loading } = useRequest(getUsername); 21 | 22 | if (error) { 23 | return
failed to load
; 24 | } 25 | if (loading) { 26 | return
loading...
; 27 | } 28 | return
Username: {data}
; 29 | }; 30 | -------------------------------------------------------------------------------- /packages/hooks/src/useRequest/doc/index/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | group: 5 | path: /use-request 6 | --- 7 | 8 | # 快速上手 9 | 10 | `useRequest` 是一个强大的异步数据管理的 Hooks,React 项目中的网络请求场景使用 `useRequest` 就够了。 11 | 12 | `useRequest` 通过插件式组织代码,核心代码极其简单,并且可以很方便的扩展出更高级的功能。目前已有能力包括: 13 | 14 | - 自动请求/手动请求 15 | - 轮询 16 | - 防抖 17 | - 节流 18 | - 屏幕聚焦重新请求 19 | - 错误重试 20 | - loading delay 21 | - SWR(stale-while-revalidate) 22 | - 缓存 23 | 24 | 接下来让我们先从两个最简单的例子认识 `useRequest`。 25 | 26 | ## 默认用法 27 | 28 | `useRequest` 的第一个参数是一个异步函数,在组件初次加载时,会自动触发该函数执行。同时自动管理该异步函数的 `loading` , `data` , `error` 等状态。 29 | 30 | ```js 31 | const { data, error, loading } = useRequest(getUsername); 32 | ``` 33 | 34 |
35 | 36 | 37 | 38 | ## 手动触发 39 | 40 | 如果设置了 `options.manual = true`,则 useRequest 不会默认执行,需要通过 `run` 来触发执行。 41 | 42 | ```js 43 | const { loading, run } = useRequest(changeUsername, { 44 | manual: true 45 | }); 46 | ``` 47 | 48 |
49 | 50 | 51 | 52 | 上面两个例子,我们演示了 `useRequest` 最基础的用法,接下来的我们开始逐个详细介绍 `useRequest` 的特性。 53 | -------------------------------------------------------------------------------- /packages/hooks/src/useRequest/doc/loadingDelay/demo/loadingDelay.tsx: -------------------------------------------------------------------------------- 1 | import { useRequest } from 'ahooks'; 2 | import React from 'react'; 3 | import Mock from 'mockjs'; 4 | 5 | function getUsername(): Promise { 6 | return new Promise((resolve) => { 7 | setTimeout(() => { 8 | resolve(Mock.mock('@name')); 9 | }, 200); 10 | }); 11 | } 12 | 13 | export default () => { 14 | const action = useRequest(getUsername); 15 | 16 | const withLoadingDelayAction = useRequest(getUsername, { 17 | loadingDelay: 300, 18 | }); 19 | 20 | const trigger = () => { 21 | action.run(); 22 | withLoadingDelayAction.run(); 23 | }; 24 | 25 | return ( 26 |
27 | 30 | 31 |
32 | Username: {action.loading ? 'Loading...' : action.data} 33 |
34 |
35 | Username: {withLoadingDelayAction.loading ? 'Loading...' : withLoadingDelayAction.data} 36 |
37 |
38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /packages/hooks/src/useRequest/doc/loadingDelay/loadingDelay.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | group: 5 | path: /use-request 6 | --- 7 | 8 | # Loading Delay 9 | 10 | 通过设置 `options.loadingDelay` ,可以延迟 `loading` 变成 `true` 的时间,有效防止闪烁。 11 | 12 | ```tsx | pure 13 | const { loading, data } = useRequest(getUsername, { 14 | loadingDelay: 300 15 | }); 16 | 17 | return
{ loading ? 'Loading...' : data }
18 | ``` 19 | 20 | 例如上面的场景,假如 `getUsername` 在 300ms 内返回,则 `loading` 不会变成 `true`,避免了页面展示 `Loading...` 的情况。 21 | 22 | 你可以快速点击下面示例中的按钮以体验效果 23 | 24 | 25 | 26 | ## API 27 | 28 | | 参数 | 说明 | 类型 | 默认值 | 29 | | ------------ | ------------------------------------- | -------- | ------ | 30 | | loadingDelay | 设置 `loading` 变成 `true` 的延迟时间 | `number` | `0` | 31 | 32 | ## 备注 33 | 34 | `options.loadingDelay` 支持动态变化。 35 | -------------------------------------------------------------------------------- /packages/hooks/src/useRequest/doc/polling/demo/polling.tsx: -------------------------------------------------------------------------------- 1 | import { useRequest } from 'ahooks'; 2 | import React from 'react'; 3 | import Mock from 'mockjs'; 4 | 5 | function getUsername() { 6 | console.log('polling getUsername'); 7 | return new Promise((resolve) => { 8 | setTimeout(() => { 9 | resolve(Mock.mock('@name')); 10 | }, 1000); 11 | }); 12 | } 13 | 14 | export default () => { 15 | const { data, loading, run, cancel } = useRequest(getUsername, { 16 | pollingInterval: 1000, 17 | pollingWhenHidden: false, 18 | }); 19 | 20 | return ( 21 | <> 22 |

Username: {loading ? 'Loading' : data}

23 | 26 | 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /packages/hooks/src/useRequest/doc/polling/demo/pollingError.tsx: -------------------------------------------------------------------------------- 1 | import { useRequest } from 'ahooks'; 2 | import React from 'react'; 3 | import Mock from 'mockjs'; 4 | import { message } from 'antd'; 5 | 6 | function getUsername() { 7 | console.log('polling getUsername Error'); 8 | return new Promise((resolve, reject) => { 9 | setTimeout(() => { 10 | reject(new Error(Mock.mock('@name'))); 11 | }, 1000); 12 | }); 13 | } 14 | 15 | export default () => { 16 | const { data, loading, run, cancel } = useRequest(getUsername, { 17 | pollingInterval: 1000, 18 | pollingWhenHidden: false, 19 | pollingErrorRetryCount: 3, 20 | manual: true, 21 | onError: (error) => { 22 | message.error(error.message); 23 | }, 24 | }); 25 | 26 | return ( 27 | <> 28 |

Username: {loading ? 'Loading' : data}

29 | 32 | 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /packages/hooks/src/useRequest/doc/ready/demo/manualReady.tsx: -------------------------------------------------------------------------------- 1 | import { useRequest, useToggle } from 'ahooks'; 2 | import Mock from 'mockjs'; 3 | import React from 'react'; 4 | 5 | function getUsername() { 6 | return new Promise((resolve) => { 7 | setTimeout(() => { 8 | resolve(Mock.mock('@name')); 9 | }, 1000); 10 | }); 11 | } 12 | 13 | export default () => { 14 | const [ready, { toggle }] = useToggle(false); 15 | 16 | const { data, loading, run } = useRequest(getUsername, { 17 | ready, 18 | manual: true, 19 | }); 20 | 21 | return ( 22 | <> 23 |

24 | Ready: {JSON.stringify(ready)} 25 | 28 |

29 |

30 | Username: {loading ? 'Loading' : data} 31 | 34 |

35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /packages/hooks/src/useRequest/doc/ready/demo/ready.tsx: -------------------------------------------------------------------------------- 1 | import { useRequest, useToggle } from 'ahooks'; 2 | import Mock from 'mockjs'; 3 | import React from 'react'; 4 | 5 | function getUsername() { 6 | return new Promise((resolve) => { 7 | setTimeout(() => { 8 | resolve(Mock.mock('@name')); 9 | }, 1000); 10 | }); 11 | } 12 | 13 | export default () => { 14 | const [ready, { toggle }] = useToggle(false); 15 | 16 | const { data, loading } = useRequest(getUsername, { 17 | ready, 18 | }); 19 | 20 | return ( 21 | <> 22 |

23 | Ready: {JSON.stringify(ready)} 24 | 27 |

28 |

Username: {loading ? 'Loading' : data}

29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /packages/hooks/src/useRequest/doc/ready/ready.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | group: 5 | path: /use-request 6 | --- 7 | 8 | # Ready 9 | 10 | 通过设置 `options.ready`,可以控制请求是否发出。当其值为 `false` 时,请求永远都不会发出。 11 | 12 | 其具体行为如下: 13 | 14 | 1. 当 `manual=false` 自动请求模式时,每次 `ready` 从 `false` 变为 `true` 时,都会自动发起请求,会带上参数 `options.defaultParams`。 15 | 2. 当 `manual=true` 手动请求模式时,只要 `ready=false`,则通过 `run/runAsync` 触发的请求都不会执行。 16 | 17 | ## 自动模式 18 | 19 | 以下示例演示了自动模式下 `ready` 的行为。每次 `ready` 从 `false` 变为 `true` 时,都会重新发起请求。 20 | 21 | 22 | 23 | ## 手动模式 24 | 25 | 以下示例演示了手动模式下 `ready` 的行为。只有当 `ready` 等于 `true` 时,`run` 才会执行。 26 | 27 | 28 | 29 | ## API 30 | 31 | ### Options 32 | 33 | | 参数 | 说明 | 类型 | 默认值 | 34 | | ----- | -------------------- | --------- | ------ | 35 | | ready | 当前请求是否准备好了 | `boolean` | `true` | 36 | -------------------------------------------------------------------------------- /packages/hooks/src/useRequest/doc/refreshOnWindowFocus/demo/refreshOnWindowFocus.tsx: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs'; 2 | import React from 'react'; 3 | import { useRequest } from 'ahooks'; 4 | 5 | function getUsername() { 6 | return new Promise((resolve) => { 7 | setTimeout(() => { 8 | resolve(Mock.mock('@name')); 9 | }, 1000); 10 | }); 11 | } 12 | 13 | export default () => { 14 | const { data, loading } = useRequest(getUsername, { 15 | refreshOnWindowFocus: true, 16 | }); 17 | 18 | return
Username: {loading ? 'Loading' : data}
; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/hooks/src/useRequest/doc/refreshOnWindowFocus/refreshOnWindowFocus.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | group: 5 | path: /use-request 6 | --- 7 | 8 | # 屏幕聚焦重新请求 9 | 10 | 通过设置 `options.refreshOnWindowFocus`,在浏览器窗口 `refocus` 和 `revisible` 时,会重新发起请求。 11 | 12 | ```tsx | pure 13 | const { data } = useRequest(getUsername, { 14 | refreshOnWindowFocus: true, 15 | }); 16 | ``` 17 | 18 | 你可以点击浏览器外部,再点击当前页面来体验效果(或者隐藏当前页面,重新展示),如果和上一次请求间隔大于 5000ms,则会重新请求一次。 19 | 20 | 21 | 22 | ## API 23 | 24 | ### Options 25 | 26 | | 参数 | 说明 | 类型 | 默认值 | 27 | | -------------------- | -------------------------------------------- | --------- | ------- | 28 | | refreshOnWindowFocus | 在屏幕重新获取焦点或重新显示时,重新发起请求 | `boolean` | `false` | 29 | | focusTimespan | 重新请求间隔,单位为毫秒 | `number` | `5000` | 30 | 31 | ## 备注 32 | 33 | - `options.refreshOnWindowFocus`、`options.focusTimespan` 支持动态变化。 34 | - 监听的浏览器事件为 `visibilitychange` 和 `focus`。 35 | -------------------------------------------------------------------------------- /packages/hooks/src/useRequest/doc/retry/demo/retry.tsx: -------------------------------------------------------------------------------- 1 | import { useRequest } from 'ahooks'; 2 | import React, { useState } from 'react'; 3 | import { message } from 'antd'; 4 | 5 | function editUsername(username: string) { 6 | return new Promise((resolve, reject) => { 7 | setTimeout(() => { 8 | reject(new Error('Failed to modify username')); 9 | }, 1000); 10 | }); 11 | } 12 | 13 | export default () => { 14 | const [state, setState] = useState(''); 15 | const { loading, run } = useRequest(editUsername, { 16 | retryCount: 3, 17 | manual: true, 18 | onError: (error) => { 19 | message.error(error.message); 20 | }, 21 | }); 22 | 23 | return ( 24 |
25 | setState(e.target.value)} 27 | value={state} 28 | placeholder="Please enter username" 29 | style={{ width: 240, marginRight: 16 }} 30 | /> 31 | 34 |
35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /packages/hooks/src/useRequest/doc/throttle/demo/throttle.tsx: -------------------------------------------------------------------------------- 1 | import { useRequest } from 'ahooks'; 2 | import Mock from 'mockjs'; 3 | import React from 'react'; 4 | 5 | async function getEmail(search?: string): Promise { 6 | console.log('throttle getEmail', search); 7 | return new Promise((resolve) => { 8 | setTimeout(() => { 9 | resolve(Mock.mock({ 'data|5': ['@email'] }).data); 10 | }, 300); 11 | }); 12 | } 13 | 14 | export default () => { 15 | const { data, loading, run } = useRequest(getEmail, { 16 | throttleWait: 1000, 17 | manual: true, 18 | }); 19 | 20 | return ( 21 |
22 | run(e.target.value)} /> 23 | {loading ? ( 24 |

loading

25 | ) : ( 26 |
    27 | {data?.map((i) => ( 28 |
  • {i}
  • 29 | ))} 30 |
31 | )} 32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /packages/hooks/src/useRequest/index.ts: -------------------------------------------------------------------------------- 1 | import useRequest from './src/useRequest'; 2 | import { clearCache } from './src/utils/cache'; 3 | 4 | export { clearCache }; 5 | 6 | export default useRequest; 7 | -------------------------------------------------------------------------------- /packages/hooks/src/useRequest/src/utils/cachePromise.ts: -------------------------------------------------------------------------------- 1 | type CachedKey = string | number; 2 | const cachePromise = new Map>(); 3 | 4 | const getCachePromise = (cacheKey: CachedKey) => { 5 | return cachePromise.get(cacheKey); 6 | }; 7 | 8 | const setCachePromise = (cacheKey: CachedKey, promise: Promise) => { 9 | // Should cache the same promise, cannot be promise.finally 10 | // Because the promise.finally will change the reference of the promise 11 | cachePromise.set(cacheKey, promise); 12 | 13 | // no use promise.finally for compatibility 14 | promise 15 | .then((res) => { 16 | cachePromise.delete(cacheKey); 17 | return res; 18 | }) 19 | .catch(() => { 20 | cachePromise.delete(cacheKey); 21 | }); 22 | }; 23 | 24 | export { getCachePromise, setCachePromise }; 25 | -------------------------------------------------------------------------------- /packages/hooks/src/useRequest/src/utils/cacheSubscribe.ts: -------------------------------------------------------------------------------- 1 | type Listener = (data: any) => void; 2 | const listeners: Record = {}; 3 | 4 | const trigger = (key: string, data: any) => { 5 | if (listeners[key]) { 6 | listeners[key].forEach((item) => item(data)); 7 | } 8 | }; 9 | 10 | const subscribe = (key: string, listener: Listener) => { 11 | if (!listeners[key]) { 12 | listeners[key] = []; 13 | } 14 | listeners[key].push(listener); 15 | 16 | return function unsubscribe() { 17 | const index = listeners[key].indexOf(listener); 18 | listeners[key].splice(index, 1); 19 | }; 20 | }; 21 | 22 | export { trigger, subscribe }; 23 | -------------------------------------------------------------------------------- /packages/hooks/src/useRequest/src/utils/isDocumentVisible.ts: -------------------------------------------------------------------------------- 1 | import isBrowser from '../../../utils/isBrowser'; 2 | 3 | export default function isDocumentVisible(): boolean { 4 | if (isBrowser) { 5 | return document.visibilityState !== 'hidden'; 6 | } 7 | return true; 8 | } 9 | -------------------------------------------------------------------------------- /packages/hooks/src/useRequest/src/utils/isOnline.ts: -------------------------------------------------------------------------------- 1 | import isBrowser from '../../../utils/isBrowser'; 2 | 3 | export default function isOnline(): boolean { 4 | if (isBrowser && typeof navigator.onLine !== 'undefined') { 5 | return navigator.onLine; 6 | } 7 | return true; 8 | } 9 | -------------------------------------------------------------------------------- /packages/hooks/src/useRequest/src/utils/limit.ts: -------------------------------------------------------------------------------- 1 | export default function limit(fn: any, timespan: number) { 2 | let pending = false; 3 | return (...args: any[]) => { 4 | if (pending) return; 5 | pending = true; 6 | fn(...args); 7 | setTimeout(() => { 8 | pending = false; 9 | }, timespan); 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /packages/hooks/src/useRequest/src/utils/subscribeFocus.ts: -------------------------------------------------------------------------------- 1 | // from swr 2 | import isBrowser from '../../../utils/isBrowser'; 3 | import isDocumentVisible from './isDocumentVisible'; 4 | import isOnline from './isOnline'; 5 | 6 | type Listener = () => void; 7 | 8 | const listeners: Listener[] = []; 9 | 10 | function subscribe(listener: Listener) { 11 | listeners.push(listener); 12 | return function unsubscribe() { 13 | const index = listeners.indexOf(listener); 14 | if (index > -1) { 15 | listeners.splice(index, 1); 16 | } 17 | }; 18 | } 19 | 20 | if (isBrowser) { 21 | const revalidate = () => { 22 | if (!isDocumentVisible() || !isOnline()) return; 23 | for (let i = 0; i < listeners.length; i++) { 24 | const listener = listeners[i]; 25 | listener(); 26 | } 27 | }; 28 | window.addEventListener('visibilitychange', revalidate, false); 29 | window.addEventListener('focus', revalidate, false); 30 | } 31 | 32 | export default subscribe; 33 | -------------------------------------------------------------------------------- /packages/hooks/src/useRequest/src/utils/subscribeReVisible.ts: -------------------------------------------------------------------------------- 1 | import isBrowser from '../../../utils/isBrowser'; 2 | import isDocumentVisible from './isDocumentVisible'; 3 | 4 | type Listener = () => void; 5 | 6 | const listeners: Listener[] = []; 7 | 8 | function subscribe(listener: Listener) { 9 | listeners.push(listener); 10 | return function unsubscribe() { 11 | const index = listeners.indexOf(listener); 12 | listeners.splice(index, 1); 13 | }; 14 | } 15 | 16 | if (isBrowser) { 17 | const revalidate = () => { 18 | if (!isDocumentVisible()) return; 19 | for (let i = 0; i < listeners.length; i++) { 20 | const listener = listeners[i]; 21 | listener(); 22 | } 23 | }; 24 | window.addEventListener('visibilitychange', revalidate, false); 25 | } 26 | 27 | export default subscribe; 28 | -------------------------------------------------------------------------------- /packages/hooks/src/useResetState/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { Button, Space } from 'antd'; 3 | import { useResetState } from 'ahooks'; 4 | 5 | export default () => { 6 | const initialValue = { 7 | hello: '', 8 | value: Math.random(), 9 | }; 10 | const initialValueMemo = useMemo(() => { 11 | return initialValue; 12 | }, []); 13 | 14 | const [state, setState, resetState] = useResetState(initialValue); 15 | 16 | return ( 17 |
18 |
initial state:
19 |
{JSON.stringify(initialValueMemo, null, 2)}
20 |
current state:
21 |
{JSON.stringify(state, null, 2)}
22 | 23 | 33 | 34 | 35 |
36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /packages/hooks/src/useResetState/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useResetState 7 | 8 | useResetState works similar to `React.useState`, it provides a `reset` method 9 | 10 | ## Examples 11 | 12 | ### Default Usage 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | const [state, setState, resetState] = useResetState( 20 | initialState: S | (() => S), 21 | ): [S, Dispatch>, () => void] 22 | ``` 23 | -------------------------------------------------------------------------------- /packages/hooks/src/useResetState/index.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from 'react'; 2 | import type { Dispatch, SetStateAction } from 'react'; 3 | import { isFunction } from '../utils'; 4 | import useMemoizedFn from '../useMemoizedFn'; 5 | import useCreation from '../useCreation'; 6 | 7 | type ResetState = () => void; 8 | 9 | const useResetState = ( 10 | initialState: S | (() => S), 11 | ): [S, Dispatch>, ResetState] => { 12 | const initialStateRef = useRef(initialState); 13 | const initialStateMemo = useCreation( 14 | () => 15 | isFunction(initialStateRef.current) ? initialStateRef.current() : initialStateRef.current, 16 | [], 17 | ); 18 | 19 | const [state, setState] = useState(initialStateMemo); 20 | 21 | const resetState = useMemoizedFn(() => { 22 | setState(initialStateMemo); 23 | }); 24 | 25 | return [state, setState, resetState]; 26 | }; 27 | 28 | export default useResetState; 29 | -------------------------------------------------------------------------------- /packages/hooks/src/useResetState/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useResetState 7 | 8 | 提供重置 state 方法的 Hooks,用法与 `React.useState` 基本一致。 9 | 10 | ## 代码演示 11 | 12 | ### 基础用法 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | const [state, setState, resetState] = useResetState( 20 | initialState: S | (() => S), 21 | ): [S, Dispatch>, () => void] 22 | ``` 23 | -------------------------------------------------------------------------------- /packages/hooks/src/useResponsive/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '../../utils/tests'; 2 | import useResponsive from '../'; 3 | 4 | describe('useResponsive', () => { 5 | function changeWidth(width: number) { 6 | act(() => { 7 | (global as any).innerWidth = width; 8 | (global as any).dispatchEvent(new Event('resize')); 9 | }); 10 | } 11 | changeWidth(1024); 12 | 13 | const hook = renderHook(() => useResponsive()); 14 | 15 | it('should response to window width changes', () => { 16 | expect(hook.result.current).toMatchSnapshot(); 17 | changeWidth(300); 18 | expect(hook.result.current).toMatchSnapshot(); 19 | changeWidth(700); 20 | expect(hook.result.current).toMatchSnapshot(); 21 | changeWidth(800); 22 | expect(hook.result.current).toMatchSnapshot(); 23 | changeWidth(1000); 24 | expect(hook.result.current).toMatchSnapshot(); 25 | changeWidth(1200); 26 | expect(hook.result.current).toMatchSnapshot(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /packages/hooks/src/useResponsive/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Get responsive info in components 3 | * desc: By calling useResponsive in components, you can retrieve the responsive infomation of the browser page and subscribe to it at the same time. 4 | * 5 | * title.zh-CN: 在组件中获取响应式信息 6 | * desc.zh-CN: 在组件中调用 useResponsive 可以获取并订阅浏览器窗口的响应式信息。 7 | */ 8 | 9 | import React from 'react'; 10 | import { configResponsive, useResponsive } from 'ahooks'; 11 | 12 | configResponsive({ 13 | small: 0, 14 | middle: 800, 15 | large: 1200, 16 | }); 17 | 18 | export default function () { 19 | const responsive = useResponsive(); 20 | return ( 21 | <> 22 |

Please change the width of the browser window to see the effect:

23 | {Object.keys(responsive).map((key) => ( 24 |

25 | {key} {responsive[key] ? '✔' : '✘'} 26 |

27 | ))} 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /packages/hooks/src/useResponsive/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useResponsive 7 | 8 | React Hook for getting responsive info. 9 | 10 | ## Examples 11 | 12 | ### Get responsive info in components 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | interface ResponsiveConfig { 20 | [key: string]: number; 21 | } 22 | interface ResponsiveInfo { 23 | [key: string]: boolean; 24 | } 25 | function configResponsive(config: ResponsiveConfig): void; 26 | function useResponsive(): ResponsiveInfo; 27 | ``` 28 | 29 | ### Config 30 | 31 | The default config is the same as bootstrap: 32 | 33 | ```javascript 34 | { 35 | 'xs': 0, 36 | 'sm': 576, 37 | 'md': 768, 38 | 'lg': 992, 39 | 'xl': 1200, 40 | } 41 | ``` 42 | 43 | If you want to config your own responsive breakpoints, you can use `configResponsive`: 44 | 45 | (Caution: You only need to config it once. Don't call this config function repeatedly.) 46 | 47 | ```javascript 48 | configResponsive({ 49 | small: 0, 50 | middle: 800, 51 | large: 1200, 52 | }); 53 | ``` 54 | -------------------------------------------------------------------------------- /packages/hooks/src/useResponsive/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useResponsive 7 | 8 | 获取响应式信息。 9 | 10 | ## 代码演示 11 | 12 | ### 在组件中获取响应式信息 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | interface ResponsiveConfig { 20 | [key: string]: number; 21 | } 22 | interface ResponsiveInfo { 23 | [key: string]: boolean; 24 | } 25 | function configResponsive(config: ResponsiveConfig): void; 26 | function useResponsive(): ResponsiveInfo; 27 | ``` 28 | 29 | ### 配置 30 | 31 | 默认的响应式配置和 bootstrap 是一致的: 32 | 33 | ```javascript 34 | { 35 | 'xs': 0, 36 | 'sm': 576, 37 | 'md': 768, 38 | 'lg': 992, 39 | 'xl': 1200, 40 | } 41 | ``` 42 | 43 | 如果你想配置自己的响应式断点,可以使用 `configResponsive` : 44 | 45 | (注意:只需配置一次,请勿在组件中重复调用该方法) 46 | 47 | ```javascript 48 | configResponsive({ 49 | small: 0, 50 | middle: 800, 51 | large: 1200, 52 | }); 53 | ``` 54 | -------------------------------------------------------------------------------- /packages/hooks/src/useSafeState/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | import { useSafeState } from 'ahooks'; 2 | import React, { useEffect, useState } from 'react'; 3 | 4 | const Child = () => { 5 | const [value, setValue] = useSafeState(); 6 | 7 | useEffect(() => { 8 | setTimeout(() => { 9 | setValue('data loaded from server'); 10 | }, 5000); 11 | }, []); 12 | 13 | const text = value || 'Loading...'; 14 | 15 | return
{text}
; 16 | }; 17 | 18 | export default () => { 19 | const [visible, setVisible] = useState(true); 20 | 21 | return ( 22 |
23 | 24 | {visible && } 25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /packages/hooks/src/useSafeState/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useSafeState 7 | 8 | It is exactly the same with `React.useState` , but after the component is unmounted, the `setState` in the asynchronous callback will no longer be executed to avoid memory leakage caused by updating the state after the component is unmounted. 9 | 10 | ## Examples 11 | 12 | ### Basic usage 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | const [state, setState] = useSafeState(initialState); 20 | ``` 21 | -------------------------------------------------------------------------------- /packages/hooks/src/useSafeState/index.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | import type { Dispatch, SetStateAction } from 'react'; 3 | import useUnmountedRef from '../useUnmountedRef'; 4 | 5 | function useSafeState(initialState: S | (() => S)): [S, Dispatch>]; 6 | 7 | function useSafeState(): [S | undefined, Dispatch>]; 8 | 9 | function useSafeState(initialState?: S | (() => S)) { 10 | const unmountedRef = useUnmountedRef(); 11 | const [state, setState] = useState(initialState); 12 | const setCurrentState = useCallback((currentState) => { 13 | /** if component is unmounted, stop update */ 14 | if (unmountedRef.current) return; 15 | setState(currentState); 16 | }, []); 17 | 18 | return [state, setCurrentState] as const; 19 | } 20 | 21 | export default useSafeState; 22 | -------------------------------------------------------------------------------- /packages/hooks/src/useSafeState/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useSafeState 7 | 8 | 用法与 `React.useState` 完全一样,但是在组件卸载后异步回调内的 `setState` 不再执行,避免因组件卸载后更新状态而导致的内存泄漏。 9 | 10 | ## 代码演示 11 | 12 | ### 基础用法 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | const [state, setState] = useSafeState(initialState); 20 | ``` 21 | -------------------------------------------------------------------------------- /packages/hooks/src/useScroll/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react'; 2 | import useScroll from '../index'; 3 | 4 | describe('useScroll', () => { 5 | it('document body', () => { 6 | const hook = renderHook(() => useScroll(document)); 7 | expect(hook.result.current).toBeUndefined(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/hooks/src/useScroll/demo/demo2.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Listen Page Scroll 3 | * desc: Try to scroll this webpage. 4 | * 5 | * title.zh-CN: 监测整页的滚动 6 | * desc.zh-CN: 尝试滚动一下页面。 7 | */ 8 | 9 | import React from 'react'; 10 | import { useScroll } from 'ahooks'; 11 | 12 | export default () => { 13 | const scroll = useScroll(document); 14 | return ( 15 |
16 |
{JSON.stringify(scroll)}
17 |
18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /packages/hooks/src/useSessionStorageState/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useSessionStorageState 7 | 8 | A Hook for store state into sessionStorage. 9 | 10 | Usage is exactly the same as [useLocalStorageState](./use-local-storage-state). 11 | -------------------------------------------------------------------------------- /packages/hooks/src/useSessionStorageState/index.ts: -------------------------------------------------------------------------------- 1 | import { createUseStorageState } from '../createUseStorageState'; 2 | import isBrowser from '../utils/isBrowser'; 3 | 4 | const useSessionStorageState = createUseStorageState(() => 5 | isBrowser ? sessionStorage : undefined, 6 | ); 7 | 8 | export default useSessionStorageState; 9 | -------------------------------------------------------------------------------- /packages/hooks/src/useSessionStorageState/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useSessionStorageState 7 | 8 | 将状态存储在 sessionStorage 中的 Hook。 9 | 10 | 用法与 [useLocalStorageState](./use-local-storage-state) 一致。 11 | -------------------------------------------------------------------------------- /packages/hooks/src/useSet/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSet } from 'ahooks'; 3 | 4 | export default () => { 5 | const [set, { add, remove, reset }] = useSet(['Hello']); 6 | 7 | return ( 8 |
9 | 12 | 20 | 23 |
24 |
{JSON.stringify(Array.from(set), null, 2)}
25 |
26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /packages/hooks/src/useSet/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useSet 7 | 8 | A hook that can manage the state of Set. 9 | 10 | ## Examples 11 | 12 | ### Default usage 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | const [ 20 | set, 21 | { 22 | add, 23 | remove, 24 | reset 25 | } 26 | ] = useSet(initialValue); 27 | ``` 28 | 29 | ### Result 30 | 31 | | Property | Description | Type | 32 | | -------- | ---------------- | ------------------ | 33 | | set | Set object | `Set` | 34 | | add | Add item | `(key: K) => void` | 35 | | remove | Remove item | `(key: K) => void` | 36 | | reset | Reset to default | `() => void` | 37 | 38 | ### Params 39 | 40 | | Property | Description | Type | Default | 41 | | ------------ | --------------------------- | ------------- | ------- | 42 | | initialValue | Optional, set default value | `Iterable` | - | 43 | -------------------------------------------------------------------------------- /packages/hooks/src/useSet/index.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import useMemoizedFn from '../useMemoizedFn'; 3 | 4 | function useSet(initialValue?: Iterable) { 5 | const getInitValue = () => new Set(initialValue); 6 | const [set, setSet] = useState>(getInitValue); 7 | 8 | const add = (key: K) => { 9 | if (set.has(key)) { 10 | return; 11 | } 12 | setSet((prevSet) => { 13 | const temp = new Set(prevSet); 14 | temp.add(key); 15 | return temp; 16 | }); 17 | }; 18 | 19 | const remove = (key: K) => { 20 | if (!set.has(key)) { 21 | return; 22 | } 23 | setSet((prevSet) => { 24 | const temp = new Set(prevSet); 25 | temp.delete(key); 26 | return temp; 27 | }); 28 | }; 29 | 30 | const reset = () => setSet(getInitValue()); 31 | 32 | return [ 33 | set, 34 | { 35 | add: useMemoizedFn(add), 36 | remove: useMemoizedFn(remove), 37 | reset: useMemoizedFn(reset), 38 | }, 39 | ] as const; 40 | } 41 | 42 | export default useSet; 43 | -------------------------------------------------------------------------------- /packages/hooks/src/useSet/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useSet 7 | 8 | 管理 Set 类型状态的 Hook。 9 | 10 | ## 代码演示 11 | 12 | 13 | 14 | ## API 15 | 16 | ```typescript 17 | const [ 18 | set, 19 | { 20 | add, 21 | remove, 22 | reset 23 | } 24 | ] = useSet(initialValue); 25 | ``` 26 | 27 | ### Result 28 | 29 | | 参数 | 说明 | 类型 | 30 | | ------ | ------------ | ------------------ | 31 | | set | Set 对象 | `Set` | 32 | | add | 添加元素 | `(key: K) => void` | 33 | | remove | 移除元素 | `(key: K) => void` | 34 | | reset | 重置为默认值 | `() => void` | 35 | 36 | ### Params 37 | 38 | | 参数 | 说明 | 类型 | 默认值 | 39 | | ------------ | --------------------------- | ------------- | ------ | 40 | | initialValue | 可选项,传入默认的 Set 参数 | `Iterable` | - | 41 | -------------------------------------------------------------------------------- /packages/hooks/src/useSetState/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Default usage 3 | * desc: Automatically merge object. 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 自动合并对象。 7 | */ 8 | 9 | import React from 'react'; 10 | import { useSetState } from 'ahooks'; 11 | 12 | interface State { 13 | hello: string; 14 | [key: string]: any; 15 | } 16 | 17 | export default () => { 18 | const [state, setState] = useSetState({ 19 | hello: '', 20 | }); 21 | 22 | return ( 23 |
24 |
{JSON.stringify(state, null, 2)}
25 |

26 | 29 | 32 |

33 |
34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /packages/hooks/src/useSetState/demo/demo2.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Updating with callback 3 | * desc: When using the callback to update, the previous state can be received, and the return value will be automatically merged. 4 | * 5 | * title.zh-CN: 使用回调更新 6 | * desc.zh-CN: 通过回调进行更新,可以获取上一次的状态,并且也会自动合并返回的对象。 7 | */ 8 | 9 | import React from 'react'; 10 | import { useSetState } from 'ahooks'; 11 | 12 | interface State { 13 | hello: string; 14 | count: number; 15 | } 16 | 17 | export default () => { 18 | const [state, setState] = useSetState({ 19 | hello: 'world', 20 | count: 0, 21 | }); 22 | 23 | return ( 24 |
25 |
{JSON.stringify(state, null, 2)}
26 |

27 | 30 |

31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/hooks/src/useSetState/index.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import useMemoizedFn from '../useMemoizedFn'; 3 | import { isFunction } from '../utils'; 4 | 5 | export type SetState> = ( 6 | state: Pick | null | ((prevState: Readonly) => Pick | S | null), 7 | ) => void; 8 | 9 | const useSetState = >( 10 | initialState: S | (() => S), 11 | ): [S, SetState] => { 12 | const [state, setState] = useState(initialState); 13 | 14 | const setMergeState = useMemoizedFn((patch) => { 15 | setState((prevState) => { 16 | const newState = isFunction(patch) ? patch(prevState) : patch; 17 | return newState ? { ...prevState, ...newState } : prevState; 18 | }); 19 | }); 20 | 21 | return [state, setMergeState]; 22 | }; 23 | 24 | export default useSetState; 25 | -------------------------------------------------------------------------------- /packages/hooks/src/useSize/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Basic usage 3 | * desc: useSize can receive ref as argument 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: useSize 可以接收 ref 参数 7 | */ 8 | 9 | import React, { useRef } from 'react'; 10 | import { useSize } from 'ahooks'; 11 | 12 | export default () => { 13 | const ref = useRef(null); 14 | const size = useSize(ref); 15 | return ( 16 |
17 |

Try to resize the preview window

18 |

19 | width: {size?.width}px, height: {size?.height}px 20 |

21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/hooks/src/useSize/demo/demo2.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: pass in the DOM element 3 | * desc: useSize can receive a dom element as parameter. In SSR scenarios, you can pass in function `() => dom` 4 | * 5 | * title.zh-CN: 传入 DOM 元素 6 | * desc.zh-CN: useSize 可以接收 dom,在 SSR 场景可以传入函数 `() => dom` 7 | */ 8 | 9 | import React from 'react'; 10 | import { useSize } from 'ahooks'; 11 | 12 | export default () => { 13 | const size = useSize(document.querySelector('body')); 14 | return ( 15 |
16 |

Try to resize the preview window

17 |

18 | width: {size?.width}px, height: {size?.height}px 19 |

20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/hooks/src/useTextSelection/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Default usage 3 | * desc: Tracking content of user text selection 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 实时获取页面上选择的文本 7 | */ 8 | 9 | import React from 'react'; 10 | import { useTextSelection } from 'ahooks'; 11 | 12 | export default () => { 13 | const { text } = useTextSelection(); 14 | return ( 15 |
16 |

You can select text all page.

17 |

Result:{text}

18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /packages/hooks/src/useTextSelection/demo/demo3.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Listen specified area 3 | * desc: useTextSelection can receive dom or ref, for listen specified area. 4 | * 5 | * title.zh-CN: 监听特定区域文本选择 6 | * desc.zh-CN: useTextSelection 可以接收 dom 或 ref,指定监听区域。 7 | */ 8 | 9 | import React, { useRef } from 'react'; 10 | import { useTextSelection } from 'ahooks'; 11 | 12 | export default () => { 13 | const ref = useRef(null); 14 | const selection = useTextSelection(ref); 15 | return ( 16 |
17 |
18 |

Please swipe your mouse to select any text on this paragraph.

19 |
20 |

Result:{JSON.stringify(selection)}

21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/hooks/src/useTheme/__test__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { act, renderHook } from '@testing-library/react'; 2 | import useTheme from '../index'; 3 | 4 | describe('useTheme', () => { 5 | test('themeMode init', () => { 6 | const { result } = renderHook(useTheme); 7 | expect(result.current.themeMode).toBe('system'); 8 | }); 9 | 10 | test('setThemeMode light', () => { 11 | const { result } = renderHook(useTheme); 12 | act(() => result.current.setThemeMode('light')); 13 | expect(result.current.theme).toBe('light'); 14 | expect(result.current.themeMode).toBe('light'); 15 | }); 16 | 17 | test('setThemeMode dark', () => { 18 | const { result } = renderHook(useTheme); 19 | act(() => result.current.setThemeMode('dark')); 20 | expect(result.current.theme).toBe('dark'); 21 | expect(result.current.themeMode).toBe('dark'); 22 | }); 23 | 24 | test('setThemeMode system', () => { 25 | const { result } = renderHook(useTheme); 26 | act(() => result.current.setThemeMode('system')); 27 | expect(result.current.themeMode).toBe('system'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/hooks/src/useThrottle/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Default usage 3 | * desc: ThrottledValue will change every 500ms. 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: ThrottledValue 每隔 500ms 变化一次。 7 | */ 8 | 9 | import React, { useState } from 'react'; 10 | import { useThrottle } from 'ahooks'; 11 | 12 | export default () => { 13 | const [value, setValue] = useState(); 14 | const throttledValue = useThrottle(value, { wait: 500 }); 15 | 16 | return ( 17 |
18 | setValue(e.target.value)} 21 | placeholder="Typed value" 22 | style={{ width: 280 }} 23 | /> 24 |

throttledValue: {throttledValue}

25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /packages/hooks/src/useThrottle/index.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import useThrottleFn from '../useThrottleFn'; 3 | import type { ThrottleOptions } from './throttleOptions'; 4 | 5 | function useThrottle(value: T, options?: ThrottleOptions) { 6 | const [throttled, setThrottled] = useState(value); 7 | 8 | const { run } = useThrottleFn(() => { 9 | setThrottled(value); 10 | }, options); 11 | 12 | useEffect(() => { 13 | run(); 14 | }, [value]); 15 | 16 | return throttled; 17 | } 18 | 19 | export default useThrottle; 20 | -------------------------------------------------------------------------------- /packages/hooks/src/useThrottle/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useThrottle 7 | 8 | 用来处理节流值的 Hook。 9 | 10 | ## 代码演示 11 | 12 | ### 基础用法 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | const throttledValue = useThrottle( 20 | value: any, 21 | options?: Options 22 | ); 23 | ``` 24 | 25 | ### Params 26 | 27 | | 参数 | 说明 | 类型 | 默认值 | 28 | | ------- | -------------- | --------- | ------ | 29 | | value | 需要节流的值 | `any` | - | 30 | | options | 配置节流的行为 | `Options` | - | 31 | 32 | ### Options 33 | 34 | | 参数 | 说明 | 类型 | 默认值 | 35 | | -------- | ------------------------ | --------- | ------ | 36 | | wait | 等待时间,单位为毫秒 | `number` | `1000` | 37 | | leading | 是否在延迟开始前调用函数 | `boolean` | `true` | 38 | | trailing | 是否在延迟开始后调用函数 | `boolean` | `true` | 39 | -------------------------------------------------------------------------------- /packages/hooks/src/useThrottle/throttleOptions.ts: -------------------------------------------------------------------------------- 1 | export interface ThrottleOptions { 2 | wait?: number; 3 | leading?: boolean; 4 | trailing?: boolean; 5 | } 6 | -------------------------------------------------------------------------------- /packages/hooks/src/useThrottleEffect/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useThrottleEffect } from 'ahooks'; 3 | 4 | export default () => { 5 | const [value, setValue] = useState('hello'); 6 | const [records, setRecords] = useState([]); 7 | useThrottleEffect( 8 | () => { 9 | setRecords((val) => [...val, value]); 10 | }, 11 | [value], 12 | { 13 | wait: 1000, 14 | }, 15 | ); 16 | return ( 17 |
18 | setValue(e.target.value)} 21 | placeholder="Typed value" 22 | style={{ width: 280 }} 23 | /> 24 |

25 |

    26 | {records.map((record, index) => ( 27 |
  • {record}
  • 28 | ))} 29 |
30 |

31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/hooks/src/useThrottleEffect/index.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import type { DependencyList, EffectCallback } from 'react'; 3 | import type { ThrottleOptions } from '../useThrottle/throttleOptions'; 4 | import useThrottleFn from '../useThrottleFn'; 5 | import useUpdateEffect from '../useUpdateEffect'; 6 | 7 | function useThrottleEffect( 8 | effect: EffectCallback, 9 | deps?: DependencyList, 10 | options?: ThrottleOptions, 11 | ) { 12 | const [flag, setFlag] = useState({}); 13 | 14 | const { run } = useThrottleFn(() => { 15 | setFlag({}); 16 | }, options); 17 | 18 | useEffect(() => { 19 | return run(); 20 | }, deps); 21 | 22 | useUpdateEffect(effect, [flag]); 23 | } 24 | 25 | export default useThrottleEffect; 26 | -------------------------------------------------------------------------------- /packages/hooks/src/useThrottleEffect/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useThrottleEffect 7 | 8 | 为 `useEffect` 增加节流的能力。 9 | 10 | ## 代码演示 11 | 12 | ### 基础用法 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | useThrottleEffect( 20 | effect: EffectCallback, 21 | deps?: DependencyList, 22 | options?: Options 23 | ); 24 | ``` 25 | 26 | ### Params 27 | 28 | | 参数 | 说明 | 类型 | 默认值 | 29 | | ------- | ---------------------------------- | ---------------- | ------ | 30 | | effect | 执行函数 | `EffectCallback` | - | 31 | | deps | 依赖数组 | `DependencyList` | - | 32 | | options | 配置节流的行为,详见下面的 Options | `Options` | - | 33 | 34 | ### Options 35 | 36 | | 参数 | 说明 | 类型 | 默认值 | 37 | | -------- | ---------------------- | --------- | ------ | 38 | | wait | 等待时间,单位为毫秒 | `number` | `1000` | 39 | | leading | 是否在在延迟开始前调用 | `boolean` | `true` | 40 | | trailing | 是否在在延迟结束后调用 | `boolean` | `true` | 41 | -------------------------------------------------------------------------------- /packages/hooks/src/useThrottleFn/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Default usage 3 | * desc: Frequent calls run, but the function is only executed every 500ms. 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 频繁调用 run,但只会每隔 500ms 执行一次相关函数。 7 | */ 8 | 9 | import React, { useState } from 'react'; 10 | import { useThrottleFn } from 'ahooks'; 11 | 12 | export default () => { 13 | const [value, setValue] = useState(0); 14 | const { run } = useThrottleFn( 15 | () => { 16 | setValue(value + 1); 17 | }, 18 | { wait: 500 }, 19 | ); 20 | 21 | return ( 22 |
23 |

Clicked count: {value}

24 | 27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /packages/hooks/src/useTimeout/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Basic usage 3 | * desc: Execute once after 3000ms 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 3000ms 后执行一次 7 | */ 8 | 9 | import React, { useState } from 'react'; 10 | import { useTimeout } from 'ahooks'; 11 | 12 | export default () => { 13 | const [state, setState] = useState(1); 14 | useTimeout(() => { 15 | setState(state + 1); 16 | }, 3000); 17 | 18 | return
{state}
; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/hooks/src/useTimeout/index.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from 'react'; 2 | import useMemoizedFn from '../useMemoizedFn'; 3 | import { isNumber } from '../utils'; 4 | 5 | const useTimeout = (fn: () => void, delay?: number) => { 6 | const timerCallback = useMemoizedFn(fn); 7 | const timerRef = useRef | null>(null); 8 | 9 | const clear = useCallback(() => { 10 | if (timerRef.current) { 11 | clearTimeout(timerRef.current); 12 | } 13 | }, []); 14 | 15 | useEffect(() => { 16 | if (!isNumber(delay) || delay < 0) { 17 | return; 18 | } 19 | timerRef.current = setTimeout(timerCallback, delay); 20 | return clear; 21 | }, [delay]); 22 | 23 | return clear; 24 | }; 25 | 26 | export default useTimeout; 27 | -------------------------------------------------------------------------------- /packages/hooks/src/useTimeout/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useTimeout 7 | 8 | 一个可以处理 setTimeout 计时器函数的 Hook。 9 | 10 | ## 代码演示 11 | 12 | ### 基础用法 13 | 14 | 15 | 16 | 17 | ## API 18 | 19 | ```typescript 20 | useTimeout( 21 | fn: () => void, 22 | delay?: number | undefined 23 | ): fn: () => void; 24 | ``` 25 | 26 | ### Params 27 | 28 | | 参数 | 说明 | 类型 | 29 | | ----- | -------------------------------------------------------------------------- | ----------------------- | 30 | | fn | 待执行函数 | `() => void` | 31 | | delay | 定时时间(单位为毫秒),支持动态变化,,当取值为 `undefined` 时会停止计时器 | `number` \| `undefined` | 32 | 33 | ### Result 34 | 35 | | 参数 | 说明 | 类型 | 36 | | ------------ | ---------- | ------------ | 37 | | clearTimeout | 清除定时器 | `() => void` | 38 | -------------------------------------------------------------------------------- /packages/hooks/src/useTitle/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Basic usage 3 | * desc: Set title of the page. 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 设置页面标题 7 | */ 8 | 9 | import React from 'react'; 10 | import { useTitle } from 'ahooks'; 11 | 12 | export default () => { 13 | useTitle('Page Title'); 14 | 15 | return ( 16 |
17 |

Set title of the page.

18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /packages/hooks/src/useTitle/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useTitle 7 | 8 | A hook that set title of the page. 9 | 10 | ## Examples 11 | 12 | ### Default usage 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | useTitle(title: string, options?: Options); 20 | ``` 21 | 22 | ### Params 23 | 24 | | Property | Description | Type | Default | 25 | | -------- | ----------- | -------- | ------- | 26 | | title | Page title | `string` | - | 27 | 28 | ### Options 29 | 30 | | Property | Description | Type | Default | 31 | | ---------------- | -------------------------------------------------------------------------- | --------- | ------- | 32 | | restoreOnUnmount | Whether to restore the previous page title when the component is unmounted | `boolean` | `false` | 33 | -------------------------------------------------------------------------------- /packages/hooks/src/useTitle/index.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import useUnmount from '../useUnmount'; 3 | import isBrowser from '../utils/isBrowser'; 4 | 5 | export interface Options { 6 | restoreOnUnmount?: boolean; 7 | } 8 | 9 | const DEFAULT_OPTIONS: Options = { 10 | restoreOnUnmount: false, 11 | }; 12 | 13 | function useTitle(title: string, options: Options = DEFAULT_OPTIONS) { 14 | const titleRef = useRef(isBrowser ? document.title : ''); 15 | useEffect(() => { 16 | document.title = title; 17 | }, [title]); 18 | 19 | useUnmount(() => { 20 | if (options.restoreOnUnmount) { 21 | document.title = titleRef.current; 22 | } 23 | }); 24 | } 25 | 26 | export default useTitle; 27 | -------------------------------------------------------------------------------- /packages/hooks/src/useTitle/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useTitle 7 | 8 | 用于设置页面标题。 9 | 10 | ## 代码演示 11 | 12 | ### 基础用法 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | useTitle(title: string, options?: Options); 20 | ``` 21 | 22 | ### Params 23 | 24 | | 参数 | 说明 | 类型 | 默认值 | 25 | | ----- | -------- | -------- | ------ | 26 | | title | 页面标题 | `string` | - | 27 | 28 | ### Options 29 | 30 | | 参数 | 说明 | 类型 | 默认值 | 31 | | ---------------- | ---------------------------------- | --------- | ------- | 32 | | restoreOnUnmount | 组件卸载时,是否恢复上一个页面标题 | `boolean` | `false` | 33 | -------------------------------------------------------------------------------- /packages/hooks/src/useToggle/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Basic usage 3 | * desc: Default value is boolean,alike useBoolean. 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 默认为 boolean 切换,基础用法与 useBoolean 一致。 7 | */ 8 | 9 | import React from 'react'; 10 | import { useToggle } from 'ahooks'; 11 | 12 | export default () => { 13 | const [state, { toggle, setLeft, setRight }] = useToggle(); 14 | 15 | return ( 16 |
17 |

Effects:{`${state}`}

18 |

19 | 22 | 25 | 28 |

29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /packages/hooks/src/useTrackedEffect/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Default usage 3 | * desc: Display the changed deps when effect function is executed. 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 查看每次 effect 执行时发生变化的依赖项 7 | */ 8 | 9 | import React, { useState } from 'react'; 10 | import { useTrackedEffect } from 'ahooks'; 11 | 12 | export default () => { 13 | const [count, setCount] = useState(0); 14 | const [count2, setCount2] = useState(0); 15 | 16 | useTrackedEffect( 17 | (changes) => { 18 | console.log('Index of changed dependencies: ', changes); 19 | }, 20 | [count, count2], 21 | ); 22 | 23 | return ( 24 |
25 |

Please open the browser console to view the output!

26 |
27 |

Count: {count}

28 | 29 |
30 |
31 |

Count2: {count2}

32 | 33 |
34 |
35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /packages/hooks/src/useTrackedEffect/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useTrackedEffect 7 | 8 | A hook of useEffect that allow us to track which dependencies caused the effect to trigger. 9 | 10 | ## Examples 11 | 12 | ### Basic usage 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | useTrackedEffect( 20 | effect: (changes: [], previousDeps: [], currentDeps: []) => (void | (() => void | undefined)), 21 | deps?: deps, 22 | ) 23 | ``` 24 | 25 | The API is alike `React.useEffect`, but the first function will receive three parameters: `changes`, `previousDeps`, and `currentDeps`. 26 | 27 | - changes: Index of changed dependencies 28 | - previousDeps: Last deps 29 | - currentDeps: Current deps 30 | -------------------------------------------------------------------------------- /packages/hooks/src/useTrackedEffect/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useTrackedEffect 7 | 8 | 追踪是哪个依赖变化触发了 `useEffect` 的执行。 9 | 10 | ## 代码演示 11 | 12 | ### 基础用法 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | useTrackedEffect( 20 | effect: (changes: [], previousDeps: [], currentDeps: []) => (void | (() => void | undefined)), 21 | deps?: deps, 22 | ) 23 | ``` 24 | 25 | API 与 `React.useEffect` 基本一致,不过第一个函数会接收 `changes`、`previousDeps`、`currentDeps` 三个参数。 26 | 27 | - changes:变化的依赖 index 数组 28 | - previousDeps:上一个依赖 29 | - currentDeps:当前依赖 30 | -------------------------------------------------------------------------------- /packages/hooks/src/useUnmount/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react'; 2 | import useUnmount from '../index'; 3 | describe('useUnmount', () => { 4 | it('useUnmount should work', async () => { 5 | const fn = jest.fn(); 6 | const hook = renderHook(() => useUnmount(fn)); 7 | expect(fn).toBeCalledTimes(0); 8 | hook.rerender(); 9 | expect(fn).toBeCalledTimes(0); 10 | hook.unmount(); 11 | expect(fn).toBeCalledTimes(1); 12 | }); 13 | 14 | // it('should output error when fn is not a function', () => { 15 | // const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); 16 | // renderHook(() => useUnmount(1 as any)); 17 | // expect(errSpy).toBeCalledWith('useUnmount expected parameter is a function, got number'); 18 | // errSpy.mockRestore(); 19 | // }); 20 | }); 21 | -------------------------------------------------------------------------------- /packages/hooks/src/useUnmount/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Basic usage 3 | * desc: The function is called right before the component is unmounted. 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 在组件卸载时,执行函数。 7 | */ 8 | 9 | import { useBoolean, useUnmount } from 'ahooks'; 10 | import { message } from 'antd'; 11 | import React from 'react'; 12 | 13 | const MyComponent = () => { 14 | useUnmount(() => { 15 | message.info('unmount'); 16 | }); 17 | 18 | return

Hello World!

; 19 | }; 20 | 21 | export default () => { 22 | const [state, { toggle }] = useBoolean(true); 23 | 24 | return ( 25 | <> 26 | 29 | {state && } 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/hooks/src/useUnmount/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useUnmount 7 | 8 | A hook that executes the function right before the component is unmounted. 9 | 10 | ## Examples 11 | 12 | ### Default Usage 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | useUnmount(fn: () => void); 20 | ``` 21 | 22 | ### Params 23 | 24 | | Property | Description | Type | Default | 25 | | -------- | --------------------------- | ------------ | ------- | 26 | | fn | The function to be executed | `() => void` | - | 27 | -------------------------------------------------------------------------------- /packages/hooks/src/useUnmount/index.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import useLatest from '../useLatest'; 3 | import { isFunction } from '../utils'; 4 | import isDev from '../utils/isDev'; 5 | 6 | const useUnmount = (fn: () => void) => { 7 | if (isDev) { 8 | if (!isFunction(fn)) { 9 | console.error(`useUnmount expected parameter is a function, got ${typeof fn}`); 10 | } 11 | } 12 | 13 | const fnRef = useLatest(fn); 14 | 15 | useEffect( 16 | () => () => { 17 | fnRef.current(); 18 | }, 19 | [], 20 | ); 21 | }; 22 | 23 | export default useUnmount; 24 | -------------------------------------------------------------------------------- /packages/hooks/src/useUnmount/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useUnmount 7 | 8 | 在组件卸载(unmount)时执行的 Hook。 9 | 10 | ## 代码演示 11 | 12 | ### 基础用法 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | useUnmount(fn: () => void); 20 | ``` 21 | 22 | ### 参数 23 | 24 | | 参数 | 说明 | 类型 | 默认值 | 25 | | ---- | -------------------- | ------------ | ------ | 26 | | fn | 组件卸载时执行的函数 | `() => void` | - | 27 | -------------------------------------------------------------------------------- /packages/hooks/src/useUnmountedRef/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '../../utils/tests'; 2 | import useUnmountedRef from '../index'; 3 | 4 | describe('useUnmountedRef', () => { 5 | it('should work', async () => { 6 | const hook = renderHook(() => useUnmountedRef()); 7 | expect(hook.result.current.current).toBe(false); 8 | hook.rerender(); 9 | expect(hook.result.current.current).toBe(false); 10 | hook.unmount(); 11 | expect(hook.result.current.current).toBe(true); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/hooks/src/useUnmountedRef/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Default usage 3 | * desc: unmountedRef.current means whether the component is unmounted 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: unmountedRef.current 代表组件是否已经卸载 7 | */ 8 | 9 | import { useBoolean, useUnmountedRef } from 'ahooks'; 10 | import { message } from 'antd'; 11 | import React, { useEffect } from 'react'; 12 | 13 | const MyComponent = () => { 14 | const unmountedRef = useUnmountedRef(); 15 | useEffect(() => { 16 | setTimeout(() => { 17 | if (!unmountedRef.current) { 18 | message.info('component is alive'); 19 | } 20 | }, 3000); 21 | }, []); 22 | 23 | return

Hello World!

; 24 | }; 25 | 26 | export default () => { 27 | const [state, { toggle }] = useBoolean(true); 28 | 29 | return ( 30 | <> 31 | 34 | {state && } 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /packages/hooks/src/useUnmountedRef/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useUnmountedRef 7 | 8 | A Hook can be used to get whether the component is unmounted. 9 | 10 | ## Examples 11 | 12 | ### Default Usage 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | const unmountRef: { current: boolean } = useUnmountedRef(); 20 | ``` 21 | 22 | ### Result 23 | 24 | | Property | Description | Type | 25 | | ---------- | ---------------------------------- | ---------------------- | 26 | | unmountRef | Whether the component is unmounted | `{ current: boolean }` | 27 | -------------------------------------------------------------------------------- /packages/hooks/src/useUnmountedRef/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | const useUnmountedRef = () => { 4 | const unmountedRef = useRef(false); 5 | useEffect(() => { 6 | unmountedRef.current = false; 7 | return () => { 8 | unmountedRef.current = true; 9 | }; 10 | }, []); 11 | return unmountedRef; 12 | }; 13 | 14 | export default useUnmountedRef; 15 | -------------------------------------------------------------------------------- /packages/hooks/src/useUnmountedRef/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useUnmountedRef 7 | 8 | 获取当前组件是否已经卸载的 Hook。 9 | 10 | ## 代码演示 11 | 12 | ### 基础用法 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | const unmountRef: { current: boolean } = useUnmountedRef(); 20 | ``` 21 | 22 | ### Result 23 | 24 | | 参数 | 说明 | 类型 | 25 | | ---------- | ---------------- | ---------------------- | 26 | | unmountRef | 组件是否已经卸载 | `{ current: boolean }` | 27 | -------------------------------------------------------------------------------- /packages/hooks/src/useUpdate/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '@testing-library/react'; 2 | import useUpdate from '..'; 3 | import useMemoizedFn from '../../useMemoizedFn'; 4 | 5 | describe('useUpdate', () => { 6 | it('should update', () => { 7 | let count = 0; 8 | const hooks = renderHook(() => { 9 | const update = useUpdate(); 10 | return { 11 | update, 12 | count, 13 | onChange: useMemoizedFn(() => { 14 | count++; 15 | update(); 16 | }), 17 | }; 18 | }); 19 | expect(hooks.result.current.count).toBe(0); 20 | act(hooks.result.current.onChange); 21 | expect(hooks.result.current.count).toBe(1); 22 | }); 23 | it('should return same update function', () => { 24 | const hooks = renderHook(() => useUpdate()); 25 | const preUpdate = hooks.result.current; 26 | hooks.rerender(); 27 | expect(hooks.result.current).toEqual(preUpdate); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/hooks/src/useUpdate/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Basic usage 3 | * desc: Forces component to re-render. 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 强制组件重新渲染。 7 | */ 8 | 9 | import React from 'react'; 10 | import { useUpdate } from 'ahooks'; 11 | 12 | export default () => { 13 | const update = useUpdate(); 14 | 15 | return ( 16 | <> 17 |
Time: {Date.now()}
18 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/hooks/src/useUpdate/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useUpdate 7 | 8 | A hook that returns a function which can be used to force the component to re-render. 9 | 10 | ## Examples 11 | 12 | ### Default Usage 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | const update = useUpdate(); 20 | ``` 21 | -------------------------------------------------------------------------------- /packages/hooks/src/useUpdate/index.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | 3 | const useUpdate = () => { 4 | const [, setState] = useState({}); 5 | 6 | return useCallback(() => setState({}), []); 7 | }; 8 | 9 | export default useUpdate; 10 | -------------------------------------------------------------------------------- /packages/hooks/src/useUpdate/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useUpdate 7 | 8 | useUpdate 会返回一个函数,调用该函数会强制组件重新渲染。 9 | 10 | ## 代码演示 11 | 12 | ### 基础用法 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | const update = useUpdate(); 20 | ``` 21 | -------------------------------------------------------------------------------- /packages/hooks/src/useUpdateEffect/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react'; 2 | import useUpdateEffect from '../index'; 3 | 4 | describe('useUpdateEffect', () => { 5 | it('test on mounted', async () => { 6 | let mountedState = 1; 7 | const hook = renderHook(() => 8 | useUpdateEffect(() => { 9 | mountedState = 2; 10 | }), 11 | ); 12 | expect(mountedState).toBe(1); 13 | hook.rerender(); 14 | expect(mountedState).toBe(2); 15 | }); 16 | it('test on optional', () => { 17 | let mountedState = 1; 18 | const hook = renderHook(() => 19 | useUpdateEffect(() => { 20 | mountedState = 3; 21 | }, [mountedState]), 22 | ); 23 | expect(mountedState).toBe(1); 24 | hook.rerender(); 25 | expect(mountedState).toBe(1); 26 | mountedState = 2; 27 | hook.rerender(); 28 | expect(mountedState).toBe(3); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/hooks/src/useUpdateEffect/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useUpdateEffect 7 | 8 | A hook alike `useEffect` but skips running the effect for the first time. 9 | 10 | ## Examples 11 | 12 | ### Basic usage 13 | 14 | 15 | 16 | ## API 17 | 18 | The API is exactly the same as `React.useEffect`. 19 | 20 | ```typescript 21 | useUpdateEffect( 22 | effect: React.EffectCallback, 23 | deps?: React.DependencyList, 24 | ) 25 | ``` 26 | -------------------------------------------------------------------------------- /packages/hooks/src/useUpdateEffect/index.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { createUpdateEffect } from '../createUpdateEffect'; 3 | 4 | export default createUpdateEffect(useEffect); 5 | -------------------------------------------------------------------------------- /packages/hooks/src/useUpdateEffect/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useUpdateEffect 7 | 8 | `useUpdateEffect` 用法等同于 `useEffect`,但是会忽略首次执行,只在依赖更新时执行。 9 | 10 | ## 代码演示 11 | 12 | ### 基础用法 13 | 14 | 15 | 16 | ## API 17 | 18 | API 与 `React.useEffect` 完全一致。 19 | 20 | ```typescript 21 | useUpdateEffect( 22 | effect: React.EffectCallback, 23 | deps?: React.DependencyList, 24 | ) 25 | ``` 26 | -------------------------------------------------------------------------------- /packages/hooks/src/useUpdateLayoutEffect/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react'; 2 | import useUpdateLayoutEffect from '../index'; 3 | 4 | describe('useUpdateLayoutEffect', () => { 5 | it('test on mounted', async () => { 6 | let mountedState = 1; 7 | const hook = renderHook(() => 8 | useUpdateLayoutEffect(() => { 9 | mountedState = 2; 10 | }), 11 | ); 12 | expect(mountedState).toBe(1); 13 | hook.rerender(); 14 | expect(mountedState).toBe(2); 15 | }); 16 | it('test on optional', () => { 17 | let mountedState = 1; 18 | const hook = renderHook(() => 19 | useUpdateLayoutEffect(() => { 20 | mountedState = 3; 21 | }, [mountedState]), 22 | ); 23 | expect(mountedState).toBe(1); 24 | hook.rerender(); 25 | expect(mountedState).toBe(1); 26 | mountedState = 2; 27 | hook.rerender(); 28 | expect(mountedState).toBe(3); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/hooks/src/useUpdateLayoutEffect/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useUpdateLayoutEffect 7 | 8 | A hook alike `useLayoutEffect` but skips running the effect for the first time. 9 | 10 | ## Examples 11 | 12 | ### Basic usage 13 | 14 | 15 | 16 | ## API 17 | 18 | The API is exactly the same as `React.useLayoutEffect`. 19 | 20 | ```typescript 21 | useUpdateEffect( 22 | effect: React.EffectCallback, 23 | deps?: React.DependencyList, 24 | ) 25 | ``` 26 | -------------------------------------------------------------------------------- /packages/hooks/src/useUpdateLayoutEffect/index.ts: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect } from 'react'; 2 | import { createUpdateEffect } from '../createUpdateEffect'; 3 | 4 | export default createUpdateEffect(useLayoutEffect); 5 | -------------------------------------------------------------------------------- /packages/hooks/src/useUpdateLayoutEffect/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useUpdateLayoutEffect 7 | 8 | `useUpdateLayoutEffect` 用法等同于 `useLayoutEffect`,但是会忽略首次执行,只在依赖更新时执行。 9 | 10 | ## 代码演示 11 | 12 | ### 基础用法 13 | 14 | 15 | 16 | ## API 17 | 18 | API 与 `React.useLayoutEffect` 完全一致。 19 | 20 | ```typescript 21 | useUpdateLayoutEffect( 22 | effect: React.EffectCallback, 23 | deps?: React.DependencyList, 24 | ) 25 | ``` 26 | -------------------------------------------------------------------------------- /packages/hooks/src/useWhyDidYouUpdate/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '@testing-library/react'; 2 | import useWhyDidYouUpdate from '../index'; 3 | import { useState } from 'react'; 4 | 5 | describe('useWhyDidYouUpdate', () => { 6 | it('should work', () => { 7 | console.log = jest.fn(); 8 | const setup = () => 9 | renderHook(() => { 10 | const [count, setCount] = useState(100); 11 | useWhyDidYouUpdate('UseWhyDidYouUpdateComponent', { count }); 12 | return { 13 | setCount, 14 | }; 15 | }); 16 | 17 | const hook = setup(); 18 | 19 | act(() => { 20 | hook.result.current.setCount(1); 21 | }); 22 | expect(console.log).toHaveBeenCalledWith( 23 | '[why-did-you-update]', 24 | 'UseWhyDidYouUpdateComponent', 25 | { 26 | count: { 27 | from: 100, 28 | to: 1, 29 | }, 30 | }, 31 | ); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/hooks/src/useWhyDidYouUpdate/index.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | export type IProps = Record; 4 | 5 | export default function useWhyDidYouUpdate(componentName: string, props: IProps) { 6 | const prevProps = useRef({}); 7 | 8 | useEffect(() => { 9 | if (prevProps.current) { 10 | const allKeys = Object.keys({ ...prevProps.current, ...props }); 11 | const changedProps: IProps = {}; 12 | 13 | allKeys.forEach((key) => { 14 | if (!Object.is(prevProps.current[key], props[key])) { 15 | changedProps[key] = { 16 | from: prevProps.current[key], 17 | to: props[key], 18 | }; 19 | } 20 | }); 21 | 22 | if (Object.keys(changedProps).length) { 23 | console.log('[why-did-you-update]', componentName, changedProps); 24 | } 25 | } 26 | 27 | prevProps.current = props; 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /packages/hooks/src/useWhyDidYouUpdate/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | path: /hooks 4 | --- 5 | 6 | # useWhyDidYouUpdate 7 | 8 | 帮助开发者排查是哪个属性改变导致了组件的 rerender。 9 | 10 | ## 代码演示 11 | 12 | ### 基础用法 13 | 14 | 15 | 16 | ## API 17 | 18 | ```typescript 19 | type IProps = Record; 20 | 21 | useWhyDidYouUpdate(componentName: string, props: IProps): void; 22 | ``` 23 | 24 | ### Params 25 | 26 | | 参数 | 说明 | 类型 | 默认值 | 27 | | ------------- | -------------------------------------------------------------------------------------- | -------- | ------ | 28 | | componentName | 必填,观测组件的名称 | `string` | - | 29 | | props | 必填,需要观测的数据(当前组件 `state` 或者传入的 `props` 等可能导致 rerender 的数据) | `object` | - | 30 | 31 | ### Result 32 | 33 | 打开控制台,可以看到改变的属性。 34 | -------------------------------------------------------------------------------- /packages/hooks/src/utils/depsAreSame.ts: -------------------------------------------------------------------------------- 1 | import type { DependencyList } from 'react'; 2 | 3 | export default function depsAreSame(oldDeps: DependencyList, deps: DependencyList): boolean { 4 | if (oldDeps === deps) return true; 5 | for (let i = 0; i < oldDeps.length; i++) { 6 | if (!Object.is(oldDeps[i], deps[i])) return false; 7 | } 8 | return true; 9 | } 10 | -------------------------------------------------------------------------------- /packages/hooks/src/utils/depsEqual.ts: -------------------------------------------------------------------------------- 1 | import type { DependencyList } from 'react'; 2 | import isEqual from 'react-fast-compare'; 3 | 4 | export const depsEqual = (aDeps: DependencyList = [], bDeps: DependencyList = []) => 5 | isEqual(aDeps, bDeps); 6 | -------------------------------------------------------------------------------- /packages/hooks/src/utils/domTarget.ts: -------------------------------------------------------------------------------- 1 | import type { MutableRefObject } from 'react'; 2 | import { isFunction } from './index'; 3 | import isBrowser from './isBrowser'; 4 | 5 | type TargetValue = T | undefined | null; 6 | 7 | type TargetType = HTMLElement | Element | Window | Document; 8 | 9 | export type BasicTarget = 10 | | (() => TargetValue) 11 | | TargetValue 12 | | MutableRefObject>; 13 | 14 | export function getTargetElement(target: BasicTarget, defaultElement?: T) { 15 | if (!isBrowser) { 16 | return undefined; 17 | } 18 | 19 | if (!target) { 20 | return defaultElement; 21 | } 22 | 23 | let targetElement: TargetValue; 24 | 25 | if (isFunction(target)) { 26 | targetElement = target(); 27 | } else if ('current' in target) { 28 | targetElement = target.current; 29 | } else { 30 | targetElement = target; 31 | } 32 | 33 | return targetElement; 34 | } 35 | -------------------------------------------------------------------------------- /packages/hooks/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const isObject = (value: unknown): value is Record => 2 | value !== null && typeof value === 'object'; 3 | export const isFunction = (value: unknown): value is (...args: any) => any => 4 | typeof value === 'function'; 5 | 6 | export const isString = (value: unknown): value is string => typeof value === 'string'; 7 | export const isBoolean = (value: unknown): value is boolean => typeof value === 'boolean'; 8 | export const isNumber = (value: unknown): value is number => typeof value === 'number'; 9 | export const isUndef = (value: unknown): value is undefined => typeof value === 'undefined'; 10 | -------------------------------------------------------------------------------- /packages/hooks/src/utils/isAppleDevice.ts: -------------------------------------------------------------------------------- 1 | const isAppleDevice = /(mac|iphone|ipod|ipad)/i.test( 2 | typeof navigator !== 'undefined' ? navigator?.platform : '', 3 | ); 4 | 5 | export default isAppleDevice; 6 | -------------------------------------------------------------------------------- /packages/hooks/src/utils/isBrowser.ts: -------------------------------------------------------------------------------- 1 | const isBrowser = !!( 2 | typeof window !== 'undefined' && 3 | window.document && 4 | window.document.createElement 5 | ); 6 | 7 | export default isBrowser; 8 | -------------------------------------------------------------------------------- /packages/hooks/src/utils/isDev.ts: -------------------------------------------------------------------------------- 1 | const isDev = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test'; 2 | 3 | export default isDev; 4 | -------------------------------------------------------------------------------- /packages/hooks/src/utils/lodash-polyfill.ts: -------------------------------------------------------------------------------- 1 | import debounce from 'lodash/debounce'; 2 | 3 | function isNodeOrWeb() { 4 | const freeGlobal = 5 | (typeof global === 'undefined' ? 'undefined' : typeof global) == 'object' && 6 | global && 7 | global.Object === Object && 8 | global; 9 | const freeSelf = typeof self == 'object' && self && self.Object === Object && self; 10 | return freeGlobal || freeSelf; 11 | } 12 | 13 | if (!isNodeOrWeb()) { 14 | global.Date = Date; 15 | } 16 | 17 | export { debounce }; 18 | -------------------------------------------------------------------------------- /packages/hooks/src/utils/rect.ts: -------------------------------------------------------------------------------- 1 | const getScrollTop = (el: Document | Element) => { 2 | if (el === document || el === document.documentElement || el === document.body) { 3 | return Math.max( 4 | window.pageYOffset, 5 | document.documentElement.scrollTop, 6 | document.body.scrollTop, 7 | ); 8 | } 9 | return (el as Element).scrollTop; 10 | }; 11 | 12 | const getScrollHeight = (el: Document | Element) => { 13 | return ( 14 | (el as Element).scrollHeight || 15 | Math.max(document.documentElement.scrollHeight, document.body.scrollHeight) 16 | ); 17 | }; 18 | 19 | const getClientHeight = (el: Document | Element) => { 20 | return ( 21 | (el as Element).clientHeight || 22 | Math.max(document.documentElement.clientHeight, document.body.clientHeight) 23 | ); 24 | }; 25 | 26 | export { getScrollTop, getScrollHeight, getClientHeight }; 27 | -------------------------------------------------------------------------------- /packages/hooks/src/utils/testingHelpers.ts: -------------------------------------------------------------------------------- 1 | export function sleep(time: number) { 2 | return new Promise((resolve) => { 3 | setTimeout(() => { 4 | resolve(); 5 | }, time); 6 | }); 7 | } 8 | 9 | export function request(req) { 10 | return new Promise((resolve, reject) => 11 | setTimeout(() => { 12 | if (req === 0) { 13 | reject(new Error('fail')); 14 | } else { 15 | resolve('success'); 16 | } 17 | }, 1000), 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/hooks/src/utils/tests.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react'; 2 | import { renderHook } from '@testing-library/react'; 3 | 4 | export * from '@testing-library/react'; 5 | 6 | const Wrapper = process.env.REACT_MODE === 'strict' ? StrictMode : undefined; 7 | 8 | const customRender: typeof renderHook = (ui, options) => 9 | renderHook(ui, { wrapper: Wrapper, ...options }); 10 | 11 | export { customRender as renderHook }; 12 | -------------------------------------------------------------------------------- /packages/hooks/src/utils/useDeepCompareWithTarget.ts: -------------------------------------------------------------------------------- 1 | import type { DependencyList, EffectCallback } from 'react'; 2 | import { useRef } from 'react'; 3 | import type { BasicTarget } from './domTarget'; 4 | import useEffectWithTarget from './useEffectWithTarget'; 5 | import { depsEqual } from './depsEqual'; 6 | 7 | const useDeepCompareEffectWithTarget = ( 8 | effect: EffectCallback, 9 | deps: DependencyList, 10 | target: BasicTarget | BasicTarget[], 11 | ) => { 12 | const ref = useRef(); 13 | const signalRef = useRef(0); 14 | 15 | if (!depsEqual(deps, ref.current)) { 16 | signalRef.current += 1; 17 | } 18 | ref.current = deps; 19 | 20 | useEffectWithTarget(effect, [signalRef.current], target); 21 | }; 22 | 23 | export default useDeepCompareEffectWithTarget; 24 | -------------------------------------------------------------------------------- /packages/hooks/src/utils/useEffectWithTarget.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import createEffectWithTarget from './createEffectWithTarget'; 3 | 4 | const useEffectWithTarget = createEffectWithTarget(useEffect); 5 | 6 | export default useEffectWithTarget; 7 | -------------------------------------------------------------------------------- /packages/hooks/src/utils/useIsomorphicLayoutEffectWithTarget.ts: -------------------------------------------------------------------------------- 1 | import isBrowser from './isBrowser'; 2 | import useEffectWithTarget from './useEffectWithTarget'; 3 | import useLayoutEffectWithTarget from './useLayoutEffectWithTarget'; 4 | 5 | const useIsomorphicLayoutEffectWithTarget = isBrowser 6 | ? useLayoutEffectWithTarget 7 | : useEffectWithTarget; 8 | 9 | export default useIsomorphicLayoutEffectWithTarget; 10 | -------------------------------------------------------------------------------- /packages/hooks/src/utils/useLayoutEffectWithTarget.ts: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect } from 'react'; 2 | import createEffectWithTarget from './createEffectWithTarget'; 3 | 4 | const useEffectWithTarget = createEffectWithTarget(useLayoutEffect); 5 | 6 | export default useEffectWithTarget; 7 | -------------------------------------------------------------------------------- /packages/hooks/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib", 6 | "composite": true, 7 | "declaration": true 8 | }, 9 | "include": ["src"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/hooks/tsconfig.pro.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.pro.json", 3 | "compilerOptions": { 4 | "rootDir": "src" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/hooks/webpack.config.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const common = require('../../webpack.common.js'); 3 | const path = require('path'); 4 | 5 | module.exports = merge(common, { 6 | entry: './es/index.js', 7 | output: { 8 | filename: 'ahooks.js', 9 | library: 'ahooks', 10 | path: path.resolve(__dirname, './dist'), 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /packages/use-url-state/README.md: -------------------------------------------------------------------------------- 1 | # useUrlState 2 | 3 | A hook that stores the state into url query parameters. 4 | 5 | ## Installing 6 | 7 | Inside your React project directory, run the following: 8 | 9 | ```bash 10 | yarn add @ahooksjs/use-url-state -S 11 | ``` 12 | 13 | Or with npm: 14 | 15 | ```bash 16 | npm install @ahooksjs/use-url-state -S 17 | ``` 18 | 19 | Or with pnpm 20 | 21 | ```bash 22 | pnpm add @ahooksjs/use-url-state 23 | ``` 24 | 25 | Or with Bun 26 | 27 | ```bash 28 | bun add @ahooksjs/use-url-state 29 | ``` 30 | 31 | ## Example 32 | 33 | ```javascript 34 | import useUrlState from '@ahooksjs/use-url-state'; 35 | 36 | const [state, setState] = useUrlState({ demoCount: '1' }); 37 | ``` 38 | 39 | ## Documentation 40 | 41 | https://ahooks.js.org/hooks/state/use-url-state 42 | -------------------------------------------------------------------------------- /packages/use-url-state/demo/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Default usage 3 | * desc: Store the state into url query. By set the value to `undefined`, the attribute can be removed from the url query. 4 | * 5 | * title.zh-CN: 基础用法 6 | * desc.zh-CN: 将状态同步到 url query 中。通过设置值为 `undefined`, 可以从 url query 上彻底删除某个属性,从而使用默认值。 7 | */ 8 | 9 | import React from 'react'; 10 | import useUrlState from '@ahooksjs/use-url-state'; 11 | 12 | export default () => { 13 | const [state, setState] = useUrlState({ count: '1' }); 14 | 15 | return ( 16 | <> 17 | 24 | 27 |
state: {state?.count}
28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /packages/use-url-state/demo/demo3.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: Custom query-string options 3 | * desc: The rules can be customized by passing in parseOptions and stringifyOptions. 4 | * 5 | * title.zh-CN: 自定义 query-string 配置 6 | * desc.zh-CN: 可以通过传入 parseOptions 和 stringifyOptions 自定义转换规则。 7 | */ 8 | 9 | import React from 'react'; 10 | import useUrlState from '@ahooksjs/use-url-state'; 11 | 12 | export default () => { 13 | const [state, setState] = useUrlState( 14 | { ids: ['1', '2', '3'] }, 15 | { 16 | parseOptions: { 17 | arrayFormat: 'comma', 18 | }, 19 | stringifyOptions: { 20 | arrayFormat: 'comma', 21 | }, 22 | }, 23 | ); 24 | 25 | return ( 26 |
27 | 37 |
ids: {JSON.stringify(state.ids)}
38 |
39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /packages/use-url-state/gulpfile.js: -------------------------------------------------------------------------------- 1 | const commonConfig = require('../../gulpfile'); 2 | 3 | exports.default = commonConfig.default; 4 | -------------------------------------------------------------------------------- /packages/use-url-state/src/__tests__/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import type { MemoryRouterProps } from 'react-router'; 4 | import { MemoryRouter, useLocation } from 'react-router'; 5 | import useUrlState from '..'; 6 | import type { Options } from '..'; 7 | 8 | export const setup = ( 9 | initialEntries: MemoryRouterProps['initialEntries'], 10 | initialState: any = {}, 11 | options?: Options, 12 | ) => { 13 | const res = {} as any; 14 | 15 | const Component = () => { 16 | const [state, setState] = useUrlState(initialState, options); 17 | const location = useLocation(); 18 | Object.assign(res, { state, setState, location }); 19 | return null; 20 | }; 21 | 22 | render( 23 | 24 | 25 | , 26 | ); 27 | 28 | return res; 29 | }; 30 | 31 | it('useUrlState should be defined', () => { 32 | expect(useUrlState).toBeDefined(); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/use-url-state/src/__tests__/router.test.tsx: -------------------------------------------------------------------------------- 1 | import { act } from '@testing-library/react'; 2 | import { setup } from '.'; 3 | 4 | describe('React Router V6', () => { 5 | it('useUrlState should be work', () => { 6 | const res = setup(['/index']); 7 | act(() => { 8 | res.setState({ count: 1 }); 9 | }); 10 | 11 | expect(res.state).toMatchObject({ count: '1' }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/use-url-state/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src" 5 | }, 6 | "include": ["src"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/use-url-state/tsconfig.pro.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.pro.json", 3 | "compilerOptions": { 4 | "rootDir": "src" 5 | }, 6 | "references": [{ "path": "../hooks" }] 7 | } 8 | -------------------------------------------------------------------------------- /packages/use-url-state/webpack.config.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const common = require('../../webpack.common.js'); 3 | const path = require('path'); 4 | 5 | module.exports = merge(common, { 6 | entry: './es/index.js', 7 | output: { 8 | filename: 'ahooks-use-url-state.js', 9 | library: 'ahooksUseUrlState', 10 | path: path.resolve(__dirname, './dist'), 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | prefer-workspace-packages: true 2 | packages: 3 | - 'packages/*' 4 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | .__dumi-default-navbar-logo { 2 | color: transparent !important; 3 | } 4 | 5 | .__dumi-default-layout-hero img { 6 | width: 250px; 7 | } 8 | 9 | a[title='站长统计'] { 10 | display: none; 11 | } 12 | 13 | input, 14 | button { 15 | padding: 4px; 16 | } 17 | 18 | input[type='number'] { 19 | border: 1px solid rgb(118, 118, 118); 20 | } 21 | 22 | [data-prefers-color='dark'] button { 23 | color: #141414; 24 | } 25 | 26 | [data-prefers-color='dark'] input { 27 | color: #141414; 28 | } 29 | 30 | #logo-version { 31 | flex: 1; 32 | margin-top: 10px; 33 | margin-left: -30px; 34 | color: #a8a8ad; 35 | font-size: 16px; 36 | } 37 | -------------------------------------------------------------------------------- /public/useExternal/test-external-script.js: -------------------------------------------------------------------------------- 1 | window.TEST_SCRIPT = { 2 | start: function () { 3 | return 'Hello World'; 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "moduleResolution": "node", 5 | "jsx": "react", 6 | "esModuleInterop": true, 7 | "downlevelIteration": true, 8 | "sourceMap": true, 9 | "baseUrl": ".", 10 | "paths": { 11 | "@/*": ["src/*"], 12 | "@ahooksjs/use-url-state": ["./packages/use-url-state/src/index.ts"] 13 | }, 14 | "allowJs": true, 15 | "allowSyntheticDefaultImports": true, 16 | "skipLibCheck": true, 17 | "declaration": false, 18 | "strictNullChecks": true, 19 | "importHelpers": true 20 | }, 21 | "exclude": ["node_modules", "lib", "es", "dist", "example"] 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.pro.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ 4 | "node_modules", 5 | "lib", 6 | "es", 7 | "dist", 8 | "**/__tests__", 9 | "**/__test__", 10 | "**/demo", 11 | "example", 12 | "gulpfile.js" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /umd.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello World 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | output: { 3 | libraryTarget: 'umd', 4 | globalObject: 'this', 5 | }, 6 | mode: 'production', 7 | resolve: { 8 | extensions: ['.json', '.js'], 9 | }, 10 | // module: { 11 | // rules: [ 12 | // { 13 | // test: /\.jsx?$/, 14 | // use: { 15 | // loader: 'babel-loader', 16 | // }, 17 | // } 18 | // ], 19 | // }, 20 | externals: [ 21 | { 22 | react: 'React', 23 | }, 24 | ], 25 | }; 26 | --------------------------------------------------------------------------------