[
  {
    "path": ".changeset/README.md",
    "content": "# Changesets\n\nHello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works\nwith multi-package repos, or single-package repos to help you version and publish your code. You can\nfind the full documentation for it [in our repository](https://github.com/changesets/changesets)\n\nWe have a quick list of common questions to get you started engaging with this project in\n[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)\n"
  },
  {
    "path": ".changeset/config.json",
    "content": "{\n    \"$schema\": \"https://unpkg.com/@changesets/config@3.1.2/schema.json\",\n    \"changelog\": \"@changesets/cli/changelog\",\n    \"commit\": false,\n    \"fixed\": [],\n    \"linked\": [[\"keepalive-for-react\", \"keepalive-for-react-router\"]],\n    \"access\": \"public\",\n    \"baseBranch\": \"main\",\n    \"updateInternalDependencies\": \"patch\",\n    \"ignore\": []\n}\n"
  },
  {
    "path": ".claude/settings.json",
    "content": "{\n    \"permissions\": {\n        \"defaultMode\": \"bypassPermissions\"\n    },\n    \"includeCoAuthoredBy\": false,\n    \"attribution\": {\n        \"commit\": \"\",\n        \"pr\": \"\"\n    },\n    \"env\": {\n        \"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS\": \"1\"\n    }\n}\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Desktop (please complete the following information):**\n - OS: [e.g. iOS]\n - Browser [e.g. chrome, safari]\n - Version [e.g. 22]\n\n**Smartphone (please complete the following information):**\n - Device: [e.g. iPhone6]\n - OS: [e.g. iOS8.1]\n - Browser [e.g. stock browser, safari]\n - Version [e.g. 22]\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/custom.md",
    "content": "---\nname: Custom issue template\nabout: Describe this issue template's purpose here.\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n\ndist\n\n\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "pnpm exec lint-staged\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"printWidth\": 140,\n  \"tabWidth\": 4,\n  \"useTabs\": false,\n  \"singleQuote\": false,\n  \"jsxSingleQuote\": false,\n  \"semi\": true,\n  \"trailingComma\": \"all\",\n  \"bracketSpacing\": true,\n  \"arrowParens\": \"avoid\"\n}\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Repository layout\n\npnpm workspace (see `pnpm-workspace.yaml`). Two publishable packages, plus two runnable examples:\n\n-   `packages/core` → `keepalive-for-react` — the `<KeepAlive/>` component and hooks. Only runtime dep is `mitt`.\n-   `packages/router` → `keepalive-for-react-router` — thin `<KeepAliveRouteOutlet/>` wrapper that reads `react-router`'s `useLocation`/`useOutlet` and feeds them into core. Peer-depends on `keepalive-for-react` and `react-router >= 6`.\n-   `examples/react-router-dom-simple-starter`, `examples/simple-tabs-starter` — Vite + React playgrounds used to smoke-test changes.\n\nVersions of the two packages are **linked** in `.changeset/config.json` — they always bump together, so a change in core should almost always have a matching changeset for router (even if router's code didn't change).\n\n## Commands\n\nAll run from repo root unless noted:\n\n```bash\npnpm install                 # install workspace\npnpm build                   # tsup build for every package (pnpm -r build)\npnpm clean                   # rm -rf dist in every package\npnpm format                  # prettier --write on ts/tsx/json\npnpm changeset               # author a changeset\npnpm version                 # apply changesets (bump versions + changelogs)\npnpm release                 # build + changeset publish\npnpm example:router          # run the react-router example\npnpm example:tabs            # run the simple-tabs example\n```\n\nPer-package build: `pnpm --filter keepalive-for-react build` (or `...-router`). Both use `tsup` with `src/index.ts` as entry, emitting `cjs`+`esm`+`.d.ts`, with `react`/`react-dom`/`react/jsx-runtime` external. There is **no test suite** — verify changes by running the examples.\n\nPre-commit: husky runs `lint-staged` which runs prettier on staged files.\n\n## Architecture\n\n### Core runtime model (`packages/core/src/components/KeepAlive/index.tsx`)\n\n`KeepAlive` holds a `cacheNodes: CacheNode[]` state (`{ cacheKey, ele, lastActiveTime, renderCount }`). On every `activeCacheKey`/`children` change (inside `useLayoutEffect` + `safeStartTransition`):\n\n-   If a node for the key exists → update its `ele` and `lastActiveTime`. If `maxAliveTime` has elapsed (global number or per-key `MaxAliveConfig[]`), bump `renderCount` (forces a remount) and emit `destroy` so `onCreate` cleanups fire.\n-   If not → push a new node. When length exceeds `max`, the LRU node (lowest `lastActiveTime`) is evicted and `destroy` is emitted for its key.\n\n`refresh`/`destroy`/`destroyAll`/`destroyOther` all emit via the `mitt` event bus (`src/event/index.ts`) before mutating state — this is the _only_ way create-time cleanups (`useEffectOnCreate`) are notified, so **any code path that removes or remounts a cache node must emit the right event first**. See commit `6c73bd1` for the precedent.\n\n`aliveRef` exposes this API imperatively via `useImperativeHandle`; `useKeepAliveRef()` is just `useRef<KeepAliveRef>(null)`.\n\n### DOM hand-off (`packages/core/src/components/CacheComponent/index.tsx`)\n\nEach cache node renders into its own imperatively-created `<div class=\"keepalive-cache-div\">` via `createPortal`. `KeepAlive` renders one `containerDivRef` element; `CacheComponent` uses a `useLayoutEffect` to move the currently-active cache div into that container and toggle `.active`/`.inactive` classes on siblings.\n\nThree swap modes, picked from props:\n\n-   **`transition`**: mark prev siblings `.inactive`, wait `duration - 40ms`, remove them, then append the new div. Relies on user CSS keying off `.active`/`.inactive`.\n-   **`viewTransition`**: wraps the sync swap in `document.startViewTransition(...)`.\n-   **default**: synchronous swap inside the same `useLayoutEffect`.\n\nWhen inactive **and** not in `include` / matching `exclude`, `CacheComponent` calls `destroy(cacheKey)` itself — this is how `include`/`exclude` eviction flows back into the parent's state.\n\nA cache node only renders its children once `activatedRef.current` has ever been true, so cached-but-never-visited children are not mounted.\n\n### Context + hooks\n\n`CacheComponentProvider` wraps each cache node's children in `CacheComponentContext`, exposing `{ active, refresh, destroy, destroyAll, destroyOther, getCacheNodes, _cacheKey }`. `_cacheKey` is the private hook used by create-time lifecycle hooks to match `destroy` events.\n\n-   `useEffectOnActive` / `useLayoutEffectOnActive` — via `useOnActive`, skip when `!active`, optional `skipMount` to no-op the first render.\n-   `useEffectOnCreate` / `useLayoutEffectOnCreate` — via `useOnCreate`, run once, store the returned cleanup, then subscribe to the event bus so cleanup fires on `destroy`/`destroyAll`/`destroyOther`/`refresh` for the matching `_cacheKey`.\n-   `useKeepAliveContext` — direct read of the context.\n\n### React version compatibility (`packages/core/src/compat/`)\n\n-   `Activity.tsx` feature-detects `React.Activity` (19.2+) and sets `hasNativeActivity`; `MemoizedActivty` delays the `visible`→`hidden` flip by `duration` ms so native Activity plays nicely with the transition swap.\n-   `safeStartTransition.ts` falls back to a synchronous call when `startTransition` isn't defined (< React 18).\n-   `enableActivity` is off by default. Turning it on changes the semantics of `useEffect` inside children: effects re-run on every activation instead of once at mount.\n\nPeer-deps allow React `>=16.8.0`. README pins the split: use `keepalive-for-react@4.x` for React 18, `@5.x` for React 19.2+. React's `StrictMode` is known-incompatible in dev and must not be used.\n\n### Router adapter (`packages/router/src/components/KeepAliveRouteOutlet/index.tsx`)\n\nTiny component: `activeCacheKey` defaults to `location.pathname + location.search`, and `children` is `useOutlet()` (optionally wrapped in `wrapperComponent`). All other `KeepAliveProps` are forwarded. If you need anything router-specific, add it here — core has no router awareness.\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Rychen Wong\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n  <img width=\"120\" src=\"./react-keepalive.png\" alt=\"keepalive-for-react logo\">\n</p>\n\n<div align=\"center\">\n  <h1 align=\"center\">\n    KeepAlive for React\n  </h1>\n</div>\n\n<p align=\"center\">A React KeepAlive component like keep-alive in vue</p>\n\n[中文](./README.zh_CN.md) | English\n\n[![NPM version](https://img.shields.io/npm/v/keepalive-for-react.svg?style=flat)](https://npmjs.com/package/keepalive-for-react) [![NPM downloads](https://img.shields.io/npm/dm/keepalive-for-react.svg?style=flat)](https://npmjs.com/package/keepalive-for-react) [![][discord-shield]][discord-link]<br/>\n\n## Packages\n\n| Package                                         | Version                                                                                                                                        | Description                  |\n| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |\n| [keepalive-for-react](./packages/core)          | [![NPM version](https://img.shields.io/npm/v/keepalive-for-react.svg?style=flat)](https://npmjs.com/package/keepalive-for-react)               | Core keepalive functionality |\n| [keepalive-for-react-router](./packages/router) | [![NPM version](https://img.shields.io/npm/v/keepalive-for-react-router.svg?style=flat)](https://npmjs.com/package/keepalive-for-react-router) | React Router integration     |\n\n## Features\n\n-   Support react-router-dom v6+ or react-router v7+\n-   Support React v16+ ~ v18+ (v19.2 Activity component support [v5.0.0])\n-   Support Suspense and Lazy import\n-   Support ErrorBoundary\n-   Support Custom Container\n-   Support Switching Animation Transition with className `active` and `inactive`\n-   Simply implement, without any extra dependencies and hacking ways\n-   Only 6KB minified size\n-   Support interrupt state effect when component is not active (v5.0.0)\n\n## Attention\n\n-   **Version Compatibility**:\n\n    -   For React 18, please use `keepalive-for-react@4.x.x`\n    -   For React 19.2+, please use `keepalive-for-react@5.x.x`\n\n-   DO NOT use <React.StrictMode />, it CANNOT work with keepalive-for-react in development mode. because it can lead to\n    some unexpected behavior.\n\n-   In Router only support react-router-dom v6+\n\n## Install\n\n```bash\nnpm install keepalive-for-react\n```\n\n```bash\nyarn add keepalive-for-react\n```\n\n```bash\npnpm add keepalive-for-react\n```\n\n## Usage\n\n### in react-router-dom v6+ or react-router v7+\n\n1. install react-router-dom v6+ or react-router v7+\n\n```bash\n# v6+\nnpm install react-router-dom keepalive-for-react keepalive-for-react-router@1.x.x\n# v7+\nnpm install react-router keepalive-for-react keepalive-for-react-router@2.x.x\n```\n\n2. use KeepAlive in your project\n\n```tsx\n// v6+ keepalive-for-react-router@1.x.x\n// v7+ keepalive-for-react-router@2.x.x\nimport KeepAliveRouteOutlet from \"keepalive-for-react-router\";\n\nfunction Layout() {\n    return (\n        <div className=\"layout\">\n            <KeepAliveRouteOutlet />\n        </div>\n    );\n}\n```\n\nor\n\n```tsx\nimport { useMemo } from \"react\";\n// v6+\nimport { useLocation, useOutlet } from \"react-router-dom\";\n// v7\n// import { useLocation, useOutlet } from \"react-router\";\nimport { KeepAlive, useKeepAliveRef } from \"keepalive-for-react\";\n\nfunction Layout() {\n    const location = useLocation();\n    const aliveRef = useKeepAliveRef();\n\n    const outlet = useOutlet();\n\n    // determine which route component to is active\n    const currentCacheKey = useMemo(() => {\n        return location.pathname + location.search;\n    }, [location.pathname, location.search]);\n\n    return (\n        <div className=\"layout\">\n            <MemoizedScrollTop>\n                <KeepAlive transition aliveRef={aliveRef} activeCacheKey={currentCacheKey} max={18}>\n                    <Suspense fallback={<LoadingArea />}>\n                        <SpreadArea>{outlet}</SpreadArea>\n                    </Suspense>\n                </KeepAlive>\n            </MemoizedScrollTop>\n        </div>\n    );\n}\n```\n\ndetails see [examples/react-router-dom-simple-starter](./examples/react-router-dom-simple-starter)\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/finedaybreak/keepalive-for-react/tree/main/examples/react-router-dom-simple-starter)\n\n### in simple tabs\n\n```bash\nnpm install keepalive-for-react\n```\n\n```tsx\nconst tabs = [\n    {\n        key: \"tab1\",\n        label: \"Tab 1\",\n        component: Tab1,\n    },\n    {\n        key: \"tab2\",\n        label: \"Tab 2\",\n        component: Tab2,\n    },\n    {\n        key: \"tab3\",\n        label: \"Tab 3\",\n        component: Tab3,\n    },\n];\n\nfunction App() {\n    const [currentTab, setCurrentTab] = useState<string>(\"tab1\");\n\n    const tab = useMemo(() => {\n        return tabs.find(tab => tab.key === currentTab);\n    }, [currentTab]);\n\n    return (\n        <div>\n            {/* ... */}\n            <KeepAlive transition={true} activeCacheKey={currentTab} exclude={[\"tab3\"]}>\n                {tab && <tab.component />}\n            </KeepAlive>\n        </div>\n    );\n}\n```\n\ndetails see [examples/simple-tabs-starter](./examples/simple-tabs-starter)\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/finedaybreak/keepalive-for-react/tree/main/examples/simple-tabs-starter)\n\n## KeepAlive Props\n\ntype definition\n\n```tsx\ninterface KeepAliveProps {\n    // determine which component to is active\n    activeCacheKey: string;\n    children?: KeepAliveChildren;\n    /**\n     * max cache count default 10\n     */\n    max?: number;\n    exclude?: Array<string | RegExp> | string | RegExp;\n    include?: Array<string | RegExp> | string | RegExp;\n    onBeforeActive?: (activeCacheKey: string) => void;\n    customContainerRef?: RefObject<HTMLDivElement>;\n    cacheNodeClassName?: string;\n    containerClassName?: string;\n    errorElement?: ComponentType<{\n        children: ReactNode;\n    }>;\n    /**\n     * transition default false\n     */\n    transition?: boolean;\n    /**\n     * use view transition to animate the component when switching tabs\n     * @see https://developer.chrome.com/docs/web-platform/view-transitions/\n     */\n    viewTransition?: boolean;\n    /**\n     * transition duration default 200\n     */\n    duration?: number;\n    aliveRef?: RefObject<KeepAliveRef | undefined>;\n    /**\n     * max alive time for cache node (second)\n     * @default 0 (no limit)\n     */\n    maxAliveTime?: number | MaxAliveConfig[];\n    /**\n     * enable Activity component from react 19.2+\n     * @default false\n     * Attention: if enable Activity component, useEffect will trigger when the component is active\n     */\n    enableActivity?: boolean;\n}\n\ninterface MaxAliveConfig {\n    match: string | RegExp;\n    expire: number;\n}\n```\n\n## Hooks\n\n### useEffectOnActive\n\n```tsx\nuseEffectOnActive(() => {\n    console.log(\"active\");\n}, []);\n```\n\n### useLayoutEffectOnActive\n\n```tsx\nuseLayoutEffectOnActive(\n    () => {\n        console.log(\"active\");\n    },\n    [],\n    false,\n);\n// the third parameter is optional, default is false,\n// if true, which means the callback will be skipped when the useLayoutEffect is triggered in first render\n```\n\n### useEffectOnCreate\n\nRun a callback only once when the component is first created (cached), and run the returned cleanup only when the component is destroyed from the cache. Unlike `useEffect(fn, [])`, it will NOT re-run when the cached component is re-activated.\n\n```tsx\nuseEffectOnCreate(() => {\n    console.log(\"component created\");\n    return () => {\n        console.log(\"component destroyed\");\n    };\n});\n```\n\n### useLayoutEffectOnCreate\n\nSame as `useEffectOnCreate` but uses `useLayoutEffect` internally. Useful when the create-time logic needs to run synchronously before the browser paints.\n\n```tsx\nuseLayoutEffectOnCreate(() => {\n    console.log(\"component created (layout)\");\n    return () => {\n        console.log(\"component destroyed (layout)\");\n    };\n});\n```\n\n### useKeepAliveContext\n\ntype definition\n\n```ts\ninterface KeepAliveContext {\n    /**\n     * whether the component is active\n     */\n    active: boolean;\n    /**\n     * refresh the component\n     * @param {string} [cacheKey] - The cache key of the component. If not provided, the current cached component will be refreshed.\n     */\n    refresh: (cacheKey?: string) => void;\n    /**\n     * destroy the component\n     * @param {string} [cacheKey] - the cache key of the component, if not provided, current active cached component will be destroyed\n     */\n    destroy: (cacheKey?: string | string[]) => Promise<void>;\n    /**\n     * destroy all components\n     */\n    destroyAll: () => Promise<void>;\n    /**\n     * destroy other components except the provided cacheKey\n     * @param {string} [cacheKey] - The cache key of the component. If not provided, destroy all components except the current active cached component.\n     */\n    destroyOther: (cacheKey?: string) => Promise<void>;\n    /**\n     * get the cache nodes\n     */\n    getCacheNodes: () => Array<CacheNode>;\n}\n```\n\n```tsx\nconst { active, refresh, destroy, getCacheNodes } = useKeepAliveContext();\n// active is a boolean, true is active, false is inactive\n// refresh is a function, you can call it to refresh the component\n// destroy is a function, you can call it to destroy the component\n// ...\n// getCacheNodes is a function, you can call it to get the cache nodes\n```\n\n### useKeepAliveRef\n\ntype definition\n\n```ts\ninterface KeepAliveRef {\n    refresh: (cacheKey?: string) => void;\n    destroy: (cacheKey?: string | string[]) => Promise<void>;\n    destroyAll: () => Promise<void>;\n    destroyOther: (cacheKey?: string) => Promise<void>;\n    getCacheNodes: () => Array<CacheNode>;\n}\n```\n\n```tsx\nfunction App() {\n    const aliveRef = useKeepAliveRef();\n    // aliveRef.current is a KeepAliveRef object\n\n    // you can call refresh and destroy on aliveRef.current\n    aliveRef.current?.refresh();\n    // it is not necessary to call destroy manually, KeepAlive will handle it automatically\n    aliveRef.current?.destroy();\n\n    return <KeepAlive aliveRef={aliveRef}>{/* ... */}</KeepAlive>;\n}\n// or\nfunction AppRouter() {\n    const aliveRef = useKeepAliveRef();\n    // aliveRef.current is a KeepAliveRef object\n\n    // you can call refresh and destroy on aliveRef.current\n    aliveRef.current?.refresh();\n    aliveRef.current?.destroy();\n    return <KeepAliveRouteOutlet aliveRef={aliveRef} />;\n}\n```\n\n## Development\n\ninstall dependencies\n\n```bash\npnpm install\n```\n\nbuild package\n\n```bash\npnpm build\n```\n\n[discord-link]: https://discord.gg/ycf896w7eA\n[discord-shield]: https://img.shields.io/discord/1232158668913381467?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square\n[discord-shield-badge]: https://img.shields.io/discord/1232158668913381467?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=for-the-badge\n"
  },
  {
    "path": "README.zh_CN.md",
    "content": "<p align=\"center\">\n  <img width=\"120\" src=\"./react-keepalive.png\" alt=\"keepalive-for-react logo\">\n</p>\n\n<div align=\"center\">\n  <h1 align=\"center\">\n    React KeepAlive 组件\n  </h1>\n</div>\n\n<p align=\"center\">一个类似Vue中keep-alive的React KeepAlive组件</p>\n\n[English](./README.md) | 中文\n\n[![NPM版本](https://img.shields.io/npm/v/keepalive-for-react.svg?style=flat)](https://npmjs.com/package/keepalive-for-react) [![NPM下载量](https://img.shields.io/npm/dm/keepalive-for-react.svg?style=flat)](https://npmjs.com/package/keepalive-for-react)\n\n## 包信息\n\n| 包名                                            | 版本                                                                                                                                       | 描述              |\n| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- |\n| [keepalive-for-react](./packages/core)          | [![NPM版本](https://img.shields.io/npm/v/keepalive-for-react.svg?style=flat)](https://npmjs.com/package/keepalive-for-react)               | 核心keepalive功能 |\n| [keepalive-for-react-router](./packages/router) | [![NPM版本](https://img.shields.io/npm/v/keepalive-for-react-router.svg?style=flat)](https://npmjs.com/package/keepalive-for-react-router) | React Router集成  |\n\n## 特性\n\n-   支持react-router-dom v6+ 或 react-router v7+\n-   支持React v16+ ~ v18+ (v19.2 Activity component support [v5.0.0])\n-   支持Suspense和懒加载导入\n-   支持错误边界\n-   支持自定义容器\n-   支持使用className `active`和`inactive`进行切换动画过渡\n-   简单实现,无需任何额外依赖和hack方式\n-   压缩后仅6KB大小\n-   支持中断state Effect当组件不活动时 (v5.0.0)\n\n## 注意事项\n\n-   **版本兼容性**：\n\n    -   React 18 请使用 `keepalive-for-react@4.x.x`\n    -   React 19.2+ 请使用 `keepalive-for-react@5.x.x`\n\n-   请勿使用 <React.StrictMode />,它在开发模式下无法与keepalive-for-react一起工作。因为它可能会导致一些意外行为。\n\n-   在路由中仅支持react-router-dom v6+\n\n## 安装\n\n```bash\nnpm install keepalive-for-react\n```\n\n```bash\nyarn add keepalive-for-react\n```\n\n```bash\npnpm add keepalive-for-react\n```\n\n## 使用\n\n### 配合react-router-dom v6+ 或 react-router v7+使用\n\n1. 安装react-router-dom v6+ 或 react-router v7+\n\n```bash\n# v6+\nnpm install react-router-dom keepalive-for-react keepalive-for-react-router@1.x.x\n# v7+\nnpm install react-router keepalive-for-react keepalive-for-react-router@2.x.x\n```\n\n2. 在项目中使用KeepAlive\n\n```tsx\n// v6+ keepalive-for-react-router@1.x.x\n// v7+ keepalive-for-react-router@2.x.x\nimport KeepAliveRouteOutlet from \"keepalive-for-react-router\";\n\nfunction Layout() {\n    return (\n        <div className=\"layout\">\n            <KeepAliveRouteOutlet />\n        </div>\n    );\n}\n```\n\n或者\n\n```tsx\nimport { useMemo } from \"react\";\nimport { useLocation } from \"react-router-dom\";\nimport { KeepAlive, useKeepAliveRef } from \"keepalive-for-react\";\n\nfunction Layout() {\n    const location = useLocation();\n    const aliveRef = useKeepAliveRef();\n\n    const outlet = useOutlet();\n\n    // 确定哪个路由组件处于活动状态\n    const currentCacheKey = useMemo(() => {\n        return location.pathname + location.search;\n    }, [location.pathname, location.search]);\n\n    return (\n        <div className=\"layout\">\n            <MemoizedScrollTop>\n                <KeepAlive transition aliveRef={aliveRef} activeCacheKey={currentCacheKey} max={18}>\n                    <Suspense fallback={<LoadingArea />}>\n                        <SpreadArea>{outlet}</SpreadArea>\n                    </Suspense>\n                </KeepAlive>\n            </MemoizedScrollTop>\n        </div>\n    );\n}\n```\n\n详情请参见 [examples/react-router-dom-simple-starter](./examples/react-router-dom-simple-starter)\n\n[![在StackBlitz中打开](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/finedaybreak/keepalive-for-react/tree/main/examples/react-router-dom-simple-starter)\n\n### 在简单标签页中\n\n```bash\nnpm install keepalive-for-react\n```\n\n```tsx\nconst tabs = [\n    {\n        key: \"tab1\",\n        label: \"标签1\",\n        component: Tab1,\n    },\n    {\n        key: \"tab2\",\n        label: \"标签2\",\n        component: Tab2,\n    },\n    {\n        key: \"tab3\",\n        label: \"标签3\",\n        component: Tab3,\n    },\n];\n\nfunction App() {\n    const [currentTab, setCurrentTab] = useState<string>(\"tab1\");\n\n    const tab = useMemo(() => {\n        return tabs.find(tab => tab.key === currentTab);\n    }, [currentTab]);\n\n    return (\n        <div>\n            {/* ... */}\n            <KeepAlive transition={true} activeCacheKey={currentTab} exclude={[\"tab3\"]}>\n                {tab && <tab.component />}\n            </KeepAlive>\n        </div>\n    );\n}\n```\n\n详情请参见 [examples/simple-tabs-starter](./examples/simple-tabs-starter)\n\n[![在StackBlitz中打开](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/finedaybreak/keepalive-for-react/tree/main/examples/simple-tabs-starter)\n\n## KeepAlive 属性\n\n类型定义\n\n```tsx\ninterface KeepAliveProps {\n    // 确定哪个组件处于活动状态\n    activeCacheKey: string;\n    children?: KeepAliveChildren;\n    /**\n     * 最大缓存数量 默认10\n     */\n    max?: number;\n    exclude?: Array<string | RegExp> | string | RegExp;\n    include?: Array<string | RegExp> | string | RegExp;\n    onBeforeActive?: (activeCacheKey: string) => void;\n    customContainerRef?: RefObject<HTMLDivElement>;\n    cacheNodeClassName?: string;\n    containerClassName?: string;\n    errorElement?: ComponentType<{\n        children: ReactNode;\n    }>;\n    /**\n     * 过渡效果 默认false\n     */\n    transition?: boolean;\n\n    /**\n     * 使用view transition来过渡组件 默认false\n     * @see https://developer.chrome.com/docs/web-platform/view-transitions/\n     */\n    viewTransition?: boolean;\n\n    /**\n     * 过渡时间 默认200ms\n     */\n    duration?: number;\n    aliveRef?: RefObject<KeepAliveRef | undefined>;\n    /**\n     * 缓存节点最大存活时间 (秒)\n     * @default 0 (无限制)\n     */\n    maxAliveTime?: number | MaxAliveConfig[];\n    /**\n     * 启用 Activity 组件 from react 19.2+\n     * @default false\n     * 注意: 如果启用 Activity 组件, useEffect 会在组件激活时触发\n     */\n    enableActivity?: boolean;\n}\n\ninterface MaxAliveConfig {\n    match: string | RegExp;\n    expire: number;\n}\n```\n\n## Hooks\n\n### useEffectOnActive\n\n```tsx\nuseEffectOnActive(() => {\n    console.log(\"active\");\n}, []);\n```\n\n### useLayoutEffectOnActive\n\n```tsx\nuseLayoutEffectOnActive(\n    () => {\n        console.log(\"active\");\n    },\n    [],\n    false,\n);\n// 第三个参数是可选的,默认为false,\n// 如果为true,表示在首次渲染时触发useLayoutEffect时会跳过回调\n```\n\n### useEffectOnCreate\n\n只在组件首次创建(加入缓存)时执行一次回调，并在组件从缓存中被销毁时执行返回的清理函数。与 `useEffect(fn, [])` 不同，被缓存的组件再次激活时 **不会** 重新执行。\n\n```tsx\nuseEffectOnCreate(() => {\n    console.log(\"组件创建\");\n    return () => {\n        console.log(\"组件销毁\");\n    };\n});\n```\n\n### useLayoutEffectOnCreate\n\n与 `useEffectOnCreate` 行为一致,内部使用 `useLayoutEffect`。适用于需要在浏览器绘制前同步执行创建逻辑的场景。\n\n```tsx\nuseLayoutEffectOnCreate(() => {\n    console.log(\"组件创建 (layout)\");\n    return () => {\n        console.log(\"组件销毁 (layout)\");\n    };\n});\n```\n\n### useKeepAliveContext\n\n类型定义\n\n```ts\ninterface KeepAliveContext {\n    /**\n     * 组件是否处于活动状态\n     */\n    active: boolean;\n    /**\n     * 刷新组件\n     * @param {string} [cacheKey] - 组件的缓存键。如果未提供，将刷新当前缓存的组件。\n     */\n    refresh: (cacheKey?: string) => void;\n    /**\n     * 销毁组件\n     * @param {string} [cacheKey] - 组件的缓存键，如果未提供，将销毁当前活动的缓存组件。\n     */\n    destroy: (cacheKey?: string | string[]) => Promise<void>;\n    /**\n     * 销毁所有组件\n     */\n    destroyAll: () => Promise<void>;\n    /**\n     * 销毁除提供的cacheKey外的其他组件\n     * @param {string} [cacheKey] - 组件的缓存键。如果未提供，将销毁除当前活动缓存组件外的所有组件。\n     */\n    destroyOther: (cacheKey?: string) => Promise<void>;\n    /**\n     * 获取缓存节点\n     */\n    getCacheNodes: () => Array<CacheNode>;\n}\n```\n\n```tsx\nconst { active, refresh, destroy, getCacheNodes } = useKeepAliveContext();\n// active 是一个布尔值，true表示活动，false表示非活动\n// refresh 是一个函数，你可以调用它来刷新组件\n// destroy 是一个函数，你可以调用它来销毁组件\n// ...\n// getCacheNodes 是一个函数，你可以调用它来获取缓存节点\n```\n\n### useKeepAliveRef\n\n类型定义\n\n```ts\ninterface KeepAliveRef {\n    refresh: (cacheKey?: string) => void;\n    destroy: (cacheKey?: string | string[]) => Promise<void>;\n    destroyAll: () => Promise<void>;\n    destroyOther: (cacheKey?: string) => Promise<void>;\n    getCacheNodes: () => Array<CacheNode>;\n}\n```\n\n```tsx\nfunction App() {\n    const aliveRef = useKeepAliveRef();\n    // aliveRef.current 是一个 KeepAliveRef 对象\n\n    // 你可以在 aliveRef.current 上调用 refresh 和 destroy\n    aliveRef.current?.refresh();\n    // 通常不需要手动调用 destroy,KeepAlive 会自动处理\n    aliveRef.current?.destroy();\n\n    return <KeepAlive aliveRef={aliveRef}>{/* ... */}</KeepAlive>;\n}\n// 或者\nfunction AppRouter() {\n    const aliveRef = useKeepAliveRef();\n    // aliveRef.current 是一个 KeepAliveRef 对象\n\n    // 你可以在 aliveRef.current 上调用 refresh 和 destroy\n    aliveRef.current?.refresh();\n    aliveRef.current?.destroy();\n    return <KeepAliveRouteOutlet aliveRef={aliveRef} />;\n}\n```\n\n## 开发\n\n安装依赖\n\n```bash\npnpm install\n```\n\n构建包\n\n```bash\npnpm build\n```\n\n[discord-link]: https://discord.gg/ycf896w7eA\n[discord-shield]: https://img.shields.io/discord/1232158668913381467?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square\n[discord-shield-badge]: https://img.shields.io/discord/1232158668913381467?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=for-the-badge\n"
  },
  {
    "path": "examples/react-router-dom-simple-starter/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "examples/react-router-dom-simple-starter/.npmrc",
    "content": "registry=https://registry.npmjs.org\n# 忽略 workspace 检查，作为独立项目运行\nignore-workspace-root-check=true\n\n"
  },
  {
    "path": "examples/react-router-dom-simple-starter/README.md",
    "content": "# React + TypeScript + Vite\n\nThis template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.\n\nCurrently, two official plugins are available:\n\n- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh\n- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh\n\n## Expanding the ESLint configuration\n\nIf you are developing a production application, we recommend updating the configuration to enable type aware lint rules:\n\n- Configure the top-level `parserOptions` property like this:\n\n```js\nexport default tseslint.config({\n  languageOptions: {\n    // other options...\n    parserOptions: {\n      project: ['./tsconfig.node.json', './tsconfig.app.json'],\n      tsconfigRootDir: import.meta.dirname,\n    },\n  },\n})\n```\n\n- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`\n- Optionally add `...tseslint.configs.stylisticTypeChecked`\n- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:\n\n```js\n// eslint.config.js\nimport react from 'eslint-plugin-react'\n\nexport default tseslint.config({\n  // Set the react version\n  settings: { react: { version: '18.3' } },\n  plugins: {\n    // Add the react plugin\n    react,\n  },\n  rules: {\n    // other rules...\n    // Enable its recommended rules\n    ...react.configs.recommended.rules,\n    ...react.configs['jsx-runtime'].rules,\n  },\n})\n```\n"
  },
  {
    "path": "examples/react-router-dom-simple-starter/eslint.config.js",
    "content": "import js from '@eslint/js'\nimport globals from 'globals'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport reactRefresh from 'eslint-plugin-react-refresh'\nimport tseslint from 'typescript-eslint'\n\nexport default tseslint.config(\n  { ignores: ['dist'] },\n  {\n    extends: [js.configs.recommended, ...tseslint.configs.recommended],\n    files: ['**/*.{ts,tsx}'],\n    languageOptions: {\n      ecmaVersion: 2020,\n      globals: globals.browser,\n    },\n    plugins: {\n      'react-hooks': reactHooks,\n      'react-refresh': reactRefresh,\n    },\n    rules: {\n      ...reactHooks.configs.recommended.rules,\n      'react-refresh/only-export-components': [\n        'warn',\n        { allowConstantExport: true },\n      ],\n    },\n  },\n)\n"
  },
  {
    "path": "examples/react-router-dom-simple-starter/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/vite.svg\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Vite + React + TS</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/react-router-dom-simple-starter/package.json",
    "content": "{\n    \"name\": \"react-router-dom-simple-starter\",\n    \"private\": true,\n    \"version\": \"0.0.0\",\n    \"type\": \"module\",\n    \"scripts\": {\n        \"dev\": \"vite\",\n        \"build\": \"tsc -b && vite build\",\n        \"lint\": \"eslint .\",\n        \"preview\": \"vite preview\"\n    },\n    \"dependencies\": {\n        \"keepalive-for-react\": \"^5.0.10\",\n        \"keepalive-for-react-router\": \"^5.0.7\",\n        \"react\": \"^19.2.5\",\n        \"react-dom\": \"^19.2.5\",\n        \"react-router\": \"^7.10.1\"\n    },\n    \"devDependencies\": {\n        \"@eslint/js\": \"^9.39.1\",\n        \"@types/react\": \"^19.2.14\",\n        \"@types/react-dom\": \"^19.2.3\",\n        \"@vitejs/plugin-react\": \"^4.7.0\",\n        \"autoprefixer\": \"^10.4.22\",\n        \"eslint\": \"^9.39.1\",\n        \"eslint-plugin-react-hooks\": \"^5.2.0\",\n        \"eslint-plugin-react-refresh\": \"^0.4.24\",\n        \"globals\": \"^15.15.0\",\n        \"postcss\": \"^8.5.6\",\n        \"tailwindcss\": \"^3.4.18\",\n        \"typescript\": \"^5.9.3\",\n        \"typescript-eslint\": \"^8.48.1\",\n        \"vite\": \"^6.4.1\"\n    },\n    \"pnpm\": {\n        \"onlyBuiltDependencies\": [\n            \"esbuild\"\n        ]\n    }\n}\n"
  },
  {
    "path": "examples/react-router-dom-simple-starter/postcss.config.js",
    "content": "export default {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}\n"
  },
  {
    "path": "examples/react-router-dom-simple-starter/src/App.tsx",
    "content": "import { Fragment } from \"react\";\nimport { RouterProvider } from \"react-router\";\nimport router from \"./router\";\n\nfunction App() {\n    return (\n        <Fragment>\n            <RouterProvider router={router}></RouterProvider>\n        </Fragment>\n    );\n}\n\nexport default App;\n"
  },
  {
    "path": "examples/react-router-dom-simple-starter/src/index.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n/* \nkeepalive-for-react animation example\ntransition should be set true to enable animation\nand duration should be set to the animation duration\n*/\n\n.cache-component.active {\n    animation: fadeIn 0.3s ease-in-out, slideIn 0.3s ease-in-out;\n}\n\n.cache-component.inactive {\n    animation: fadeOut 0.3s ease-in-out, slideOut 0.3s ease-in-out;\n}\n\n@keyframes fadeIn {\n    from {\n        opacity: 0;\n    }\n    to {\n        opacity: 1;\n    }\n}\n\n@keyframes fadeOut {\n    from {\n        opacity: 1;\n    }\n    to {\n        opacity: 0;\n    }\n}\n\n@keyframes slideIn {\n    from {\n        transform: translateX(-100%);\n    }\n    to {\n        transform: translateX(0);\n    }\n}\n\n@keyframes slideOut {\n    from {\n        transform: translateX(0);\n    }\n    to {\n        transform: translateX(100%);\n    }\n}"
  },
  {
    "path": "examples/react-router-dom-simple-starter/src/layout/index.tsx",
    "content": "import { useKeepAliveRef } from \"keepalive-for-react\";\nimport KeepAliveRouteOutlet from \"keepalive-for-react-router\";\nimport { ReactNode, Suspense, useEffect, useMemo, useRef } from \"react\";\nimport { Link, useLocation } from \"react-router\";\n\nfunction Layout() {\n    const location = useLocation();\n    const activePath = location.pathname + location.search;\n    const aliveRef = useKeepAliveRef();\n    return (\n        <div className=\"text-neutral-700 overflow-hidden max-w-[600px] mx-auto my-[20px] border border-neutral-200 rounded-md\">\n            <div className=\"flex md:text-[16px] text-[12px] gap-4 h-[40px] justify-around items-center  px-4 bg-gray-100\">\n                <Link to=\"/\" className={activePath === \"/\" ? \"text-blue-500\" : \"\"}>\n                    Home\n                </Link>\n                <Link to=\"/about\" className={activePath === \"/about\" ? \"text-blue-500\" : \"\"}>\n                    About\n                </Link>\n                <Link to=\"/counter\" className={activePath === \"/counter\" ? \"text-blue-500\" : \"\"}>\n                    Counter\n                </Link>\n                <Link to=\"/nocache-counter\" className={activePath === \"/nocache-counter\" ? \"text-blue-500\" : \"\"}>\n                    Counter2\n                </Link>\n                <Link to=\"/nested/nested-a\" className={activePath === \"/nested/nested-a\" ? \"text-blue-500\" : \"\"}>\n                    NestedA\n                </Link>\n                <Link to=\"/nested/nested-b\" className={activePath === \"/nested/nested-b\" ? \"text-blue-500\" : \"\"}>\n                    NestedB\n                </Link>\n            </div>\n            <div>\n                <CustomSuspense>\n                    <KeepAliveRouteOutlet\n                        wrapperComponent={MemoScrollTopWrapper}\n                        duration={300}\n                        transition={true}\n                        exclude={[\"/nocache-counter\"]}\n                        aliveRef={aliveRef}\n                        enableActivity={true}\n                    />\n                </CustomSuspense>\n            </div>\n        </div>\n    );\n}\n\n// remember the scroll position of the page when switching routes\nfunction MemoScrollTopWrapper(props: { children?: ReactNode }) {\n    const { children } = props;\n    const domRef = useRef<HTMLDivElement>(null);\n    const location = useLocation();\n    const scrollHistoryMap = useRef<Map<string, number>>(new Map());\n\n    const activeKey = useMemo(() => {\n        return location.pathname + location.search;\n    }, [location.pathname, location.search]);\n\n    useEffect(() => {\n        const divDom = domRef.current;\n        if (!divDom) return;\n        setTimeout(() => {\n            divDom.scrollTo(0, scrollHistoryMap.current.get(activeKey) || 0);\n        }, 300); // 300 milliseconds to wait for the animation transition ending\n        const onScroll = (e: Event) => {\n            const target = e.target as HTMLDivElement;\n            if (!target) return;\n            scrollHistoryMap.current.set(activeKey, target?.scrollTop || 0);\n        };\n        divDom?.addEventListener(\"scroll\", onScroll, {\n            passive: true,\n        });\n        return () => {\n            divDom?.removeEventListener(\"scroll\", onScroll);\n        };\n    }, [activeKey]);\n\n    return (\n        <div\n            className=\"animation-wrapper scrollbar w-full overflow-auto\"\n            style={{\n                height: \"calc(100vh - 82px)\",\n            }}\n            ref={domRef}\n        >\n            {children}\n        </div>\n    );\n}\n\nfunction CustomSuspense(props: { children: ReactNode }) {\n    const { children } = props;\n    return <Suspense fallback={<div className=\"text-center text-red-400 text-[12px] mt-[10px]\">Loading...</div>}>{children}</Suspense>;\n}\n\nexport default Layout;\n"
  },
  {
    "path": "examples/react-router-dom-simple-starter/src/main.tsx",
    "content": "import { createRoot } from \"react-dom/client\";\nimport App from \"./App.tsx\";\nimport \"./index.css\";\n\ncreateRoot(document.getElementById(\"root\")!).render(<App />);\n"
  },
  {
    "path": "examples/react-router-dom-simple-starter/src/pages/about/index.tsx",
    "content": "function About() {\n    return (\n        <div className=\"p-[20px]\">\n            <h1 className=\"text-center text-xl font-bold py-[10px]\">About</h1>\n            <div>\n                <textarea className=\"w-full h-[200px] bg-neutral-50 p-[15px]\" defaultValue=\"Hello World\"></textarea>\n            </div>\n        </div>\n    );\n}\n\nexport default About;\n"
  },
  {
    "path": "examples/react-router-dom-simple-starter/src/pages/counter/index.tsx",
    "content": "import { useEffectOnActive, useEffectOnCreate, useKeepAliveContext } from \"keepalive-for-react\";\nimport { useState } from \"react\";\n\nfunction Counter() {\n    const [count, setCount] = useState(0);\n    const { refresh, active } = useKeepAliveContext();\n\n    // useEffectOnActive(() => {\n    //     console.log(\"Counter is active (OnActive)\", count);\n    //     return () => {\n    //         console.log(\"Counter is destroyed (OnActive)\", count);\n    //     };\n    // }, [count]);\n\n    useEffectOnCreate(() => {\n        console.log(\"Counter is created (OnCreate)\", count);\n        return () => {\n            console.log(\"Counter is destroyed (_OnCreate)\", count);\n        };\n    });\n\n    return (\n        <div className=\"p-[20px]\">\n            <h1 className=\"text-center text-xl font-bold py-[10px]\">Counter</h1>\n            <div className=\"status text-[12px] mb-[10px] text-center text-white \">\n                <span className={active ? \"bg-green-600 rounded-md px-[5px] py-[2px]\" : \"bg-red-500 rounded-md px-[5px] py-[2px]\"}>\n                    Active: {active ? \"true\" : \"false\"}\n                </span>\n            </div>\n            <div className=\"text-center text-xl py-[10px] rounded-md bg-neutral-50 p-[10px]\">{count}</div>\n            <div className=\"flex gap-4 justify-center mt-[15px]\">\n                <button\n                    className=\"px-[10px] py-[5px] bg-blue-500 active:bg-blue-600 text-white rounded-md\"\n                    onClick={() => setCount(count + 1)}\n                >\n                    Increase\n                </button>\n                <button\n                    className=\"px-[10px] py-[5px] bg-pink-500 active:bg-pink-600 text-white rounded-md\"\n                    onClick={() => setCount(count - 1)}\n                >\n                    Decrease\n                </button>\n                <button className=\"px-[10px] py-[5px] bg-green-500 active:bg-green-600 text-white rounded-md\" onClick={() => refresh()}>\n                    Refresh\n                </button>\n            </div>\n        </div>\n    );\n}\n\nexport default Counter;\n"
  },
  {
    "path": "examples/react-router-dom-simple-starter/src/pages/home/index.tsx",
    "content": "import { useEffectOnActive } from \"keepalive-for-react\";\nimport { useRef } from \"react\";\n\nfunction Home() {\n    const domRef = useRef<HTMLDivElement>(null);\n    useEffectOnActive(\n        () => {\n            console.log(\"Home is active\");\n            const dom = domRef.current;\n            if (dom) {\n                // if transition is true, the dom size will be 0, because the dom is not rendered yet\n                console.log(`home dom size: height ${dom.clientHeight}px  width ${dom.clientWidth}px`);\n                setTimeout(() => {\n                    console.log(`home dom size: height ${dom.clientHeight}px  width ${dom.clientWidth}px`);\n                }, 300);\n            }\n        },\n        [],\n        true,\n    );\n\n    return (\n        <div className=\"p-[20px]\" ref={domRef}>\n            <h1 className=\"text-center text-xl font-bold py-[10px]\">Home</h1>\n            <p className=\"text-center text-neutral-500\">\n                Welcome to the home page, this is a simple example of how to use keepalive-for-react with react-router-dom.\n            </p>\n            <h2 className=\"text-lg font-bold mt-[10px] mb-[5px]\">Install</h2>\n            <code className=\"block w-full bg-gray-100 p-[10px] rounded-md\">\n                <pre className=\"text-[12px] whitespace-pre-wrap\">{`npm install keepalive-for-react react-router-dom`}</pre>\n            </code>\n            <div className=\" text-neutral-500 mt-[30px]\">{\"./src/layout/index.tsx\"}</div>\n            <code className=\"block w-full bg-gray-100 p-[10px] rounded-md mt-[10px]\">\n                <pre className=\"text-[12px] whitespace-pre-wrap\">{`<KeepAliveRouteOutlet duration={300} transition={true} />`}</pre>\n            </code>\n            <div className=\" mt-[30px]\">\n                {\"Github: \"}\n                <a className=\"text-blue-500\" target=\"_blank\" href=\"https://github.com/finedaybreak/keepalive-for-react\">\n                    {\"https://github.com/finedaybreak/keepalive-for-react\"}\n                </a>\n            </div>\n            <div className=\"min-h-[600px]\"></div>\n        </div>\n    );\n}\n\nexport default Home;\n"
  },
  {
    "path": "examples/react-router-dom-simple-starter/src/pages/nested/index.tsx",
    "content": "import { Outlet } from \"react-router\";\n\nfunction Nested() {\n    return (\n        <div>\n            <h1 className=\"text-center text-xl font-bold py-[10px]\">Nested</h1>\n            <Outlet />\n        </div>\n    );\n}\n\nexport default Nested;\n"
  },
  {
    "path": "examples/react-router-dom-simple-starter/src/pages/nested/nested-a/index.tsx",
    "content": "function NestedA() {\n    return (\n        <div className=\"mx-10 p-10 bg-gray-100 rounded-lg min-h-[200px]\">\n            <div className=\"text-md font-bold py-[10px]\">NestedA</div>\n            <p className=\"text-sm text-gray-500 mb-2\">This is a nested route. It will be cached.</p>\n            <input type=\"text\" className=\"border border-gray-300 rounded-md p-1 px-2\" placeholder=\"input text\" />\n        </div>\n    );\n}\n\nexport default NestedA;\n"
  },
  {
    "path": "examples/react-router-dom-simple-starter/src/pages/nested/nested-b/index.tsx",
    "content": "function NestedB() {\n    return (\n        <div className=\"mx-10 p-10 bg-gray-100 rounded-lg min-h-[200px]\">\n            <div className=\"text-md font-bold py-[10px]\">NestedB</div>\n            <p className=\"text-sm text-gray-500 mb-2\">This is a nested route. It will be cached.</p>\n            <input type=\"text\" className=\"border border-gray-300 rounded-md p-1 px-2\" placeholder=\"input text\" />\n        </div>\n    );\n}\n\nexport default NestedB;\n"
  },
  {
    "path": "examples/react-router-dom-simple-starter/src/pages/nocache-counter/index.tsx",
    "content": "import { useEffectOnCreate, useKeepAliveContext, useLayoutEffectOnCreate } from \"keepalive-for-react\";\nimport { useState } from \"react\";\n\nfunction NoCacheCounter() {\n    const [count, setCount] = useState(0);\n    const { refresh, destroy } = useKeepAliveContext();\n    useEffectOnCreate(() => {\n        console.log(\"NoCacheCounter is created (OnCreate)\", count);\n        return () => {\n            console.log(\"NoCacheCounter is destroyed (OnCreate)\", count);\n        };\n    });\n    useLayoutEffectOnCreate(() => {\n        console.log(\"NoCacheCounter is created (OnLayoutCreate)\", count);\n        return () => {\n            console.log(\"NoCacheCounter is destroyed (OnLayoutCreate)\", count);\n        };\n    });\n    return (\n        <div className=\"p-[20px]\">\n            <h1 className=\"text-center text-xl font-bold py-[10px]\">Counter (No Cache)</h1>\n            <div className=\"text-center text-xl py-[10px] rounded-md bg-neutral-50 p-[10px]\">{count}</div>\n            <div className=\"flex gap-4 justify-center mt-[15px]\">\n                <button\n                    className=\"px-[10px] py-[5px] bg-blue-500 active:bg-blue-600 text-white rounded-md\"\n                    onClick={() => setCount(count + 1)}\n                >\n                    Increase\n                </button>\n                <button\n                    className=\"px-[10px] py-[5px] bg-pink-500 active:bg-pink-600 text-white rounded-md\"\n                    onClick={() => setCount(count - 1)}\n                >\n                    Decrease\n                </button>\n                <button className=\"px-[10px] py-[5px] bg-green-500 active:bg-green-600 text-white rounded-md\" onClick={() => refresh()}>\n                    Refresh\n                </button>\n                <button\n                    className=\"px-[10px] py-[5px] bg-red-500 active:bg-red-600 text-white rounded-md\"\n                    onClick={() => destroy(\"/counter\")}\n                >\n                    Destroy\n                </button>\n            </div>\n        </div>\n    );\n}\n\nexport default NoCacheCounter;\n"
  },
  {
    "path": "examples/react-router-dom-simple-starter/src/router/index.tsx",
    "content": "import { createBrowserRouter } from \"react-router\";\nimport Layout from \"../layout\";\nimport { lazy } from \"react\";\nimport Nested from \"../pages/nested\";\nimport NestedA from \"../pages/nested/nested-a\";\nimport NestedB from \"../pages/nested/nested-b\";\n// import Home from \"../pages/home\";\n// import About from \"../pages/about\";\n// import Counter from \"../pages/counter\";\n// import NoCacheCounter from \"../pages/nocache-counter\";\n\n// lazy load\nconst Home = lazy(() => import(\"../pages/home\"));\nconst About = lazy(() => import(\"../pages/about\"));\nconst Counter = lazy(() => import(\"../pages/counter\"));\nconst NoCacheCounter = lazy(() => import(\"../pages/nocache-counter\"));\n\nconst router = createBrowserRouter([\n    {\n        path: \"/\",\n        element: <Layout />,\n        children: [\n            {\n                path: \"\",\n                element: <Home />,\n            },\n            {\n                path: \"/about\",\n                element: <About />,\n            },\n            {\n                path: \"/counter\",\n                element: <Counter />,\n            },\n            {\n                path: \"/nocache-counter\",\n                element: <NoCacheCounter />,\n            },\n            {\n                path: \"/nested\",\n                element: <Nested />,\n                children: [\n                    {\n                        path: \"nested-a\",\n                        element: <NestedA />,\n                    },\n                    {\n                        path: \"nested-b\",\n                        element: <NestedB />,\n                    },\n                ],\n            },\n        ],\n    },\n]);\n\nexport default router;\n"
  },
  {
    "path": "examples/react-router-dom-simple-starter/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/react-router-dom-simple-starter/tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\nexport default {\n  content: [\n    \"./index.html\",\n    \"./src/**/*.{js,ts,jsx,tsx}\",\n  ],\n  theme: {\n    extend: {},\n  },\n  plugins: [],\n}\n\n"
  },
  {
    "path": "examples/react-router-dom-simple-starter/tsconfig.app.json",
    "content": "{\n    \"compilerOptions\": {\n        \"target\": \"ES2020\",\n        \"useDefineForClassFields\": true,\n        \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n        \"module\": \"ESNext\",\n        \"skipLibCheck\": true,\n\n        /* Bundler mode */\n        \"moduleResolution\": \"bundler\",\n        \"allowImportingTsExtensions\": true,\n        \"isolatedModules\": true,\n        \"moduleDetection\": \"force\",\n        \"noEmit\": true,\n        \"jsx\": \"react-jsx\",\n\n        /* Linting */\n        \"strict\": true,\n        \"noUnusedLocals\": true,\n        \"noUnusedParameters\": true,\n        \"noFallthroughCasesInSwitch\": true\n    },\n    \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "examples/react-router-dom-simple-starter/tsconfig.app.tsbuildinfo",
    "content": "{\"root\":[\"./src/app.tsx\",\"./src/main.tsx\",\"./src/vite-env.d.ts\",\"./src/layout/index.tsx\",\"./src/pages/about/index.tsx\",\"./src/pages/counter/index.tsx\",\"./src/pages/home/index.tsx\",\"./src/pages/nested/index.tsx\",\"./src/pages/nested/nested-a/index.tsx\",\"./src/pages/nested/nested-b/index.tsx\",\"./src/pages/nocache-counter/index.tsx\",\"./src/router/index.tsx\"],\"errors\":true,\"version\":\"5.9.3\"}"
  },
  {
    "path": "examples/react-router-dom-simple-starter/tsconfig.json",
    "content": "{\n    \"files\": [],\n    \"references\": [{ \"path\": \"./tsconfig.app.json\" }, { \"path\": \"./tsconfig.node.json\" }]\n}\n"
  },
  {
    "path": "examples/react-router-dom-simple-starter/tsconfig.node.json",
    "content": "{\n    \"compilerOptions\": {\n        \"target\": \"ES2022\",\n        \"lib\": [\"ES2023\"],\n        \"module\": \"ESNext\",\n        \"skipLibCheck\": true,\n\n        /* Bundler mode */\n        \"moduleResolution\": \"bundler\",\n        \"allowImportingTsExtensions\": true,\n        \"isolatedModules\": true,\n        \"moduleDetection\": \"force\",\n        \"noEmit\": true,\n\n        /* Linting */\n        \"strict\": true,\n        \"noUnusedLocals\": true,\n        \"noUnusedParameters\": true,\n        \"noFallthroughCasesInSwitch\": true\n    },\n    \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "examples/react-router-dom-simple-starter/tsconfig.node.tsbuildinfo",
    "content": "{\"root\":[\"./vite.config.ts\"],\"version\":\"5.9.3\"}"
  },
  {
    "path": "examples/react-router-dom-simple-starter/vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\n// import path from \"node:path\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n    plugins: [react()],\n    // resolve: {\n    //     alias: {\n    //         \"keepalive-for-react\": path.resolve(__dirname, \"../../packages/core/src/index.ts\"),\n    //         \"keepalive-for-react-router\": path.resolve(__dirname, \"../../packages/router/src/index.ts\"),\n    //     },\n    //     dedupe: [\"react\", \"react-dom\"],\n    // },\n});\n"
  },
  {
    "path": "examples/simple-tabs-starter/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "examples/simple-tabs-starter/.npmrc",
    "content": "registry=https://registry.npmjs.org\n# 忽略 workspace 检查，作为独立项目运行\nignore-workspace-root-check=true\n\n"
  },
  {
    "path": "examples/simple-tabs-starter/README.md",
    "content": "# React + TypeScript + Vite\n\nThis template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.\n\nCurrently, two official plugins are available:\n\n- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh\n- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh\n\n## Expanding the ESLint configuration\n\nIf you are developing a production application, we recommend updating the configuration to enable type aware lint rules:\n\n- Configure the top-level `parserOptions` property like this:\n\n```js\nexport default tseslint.config({\n  languageOptions: {\n    // other options...\n    parserOptions: {\n      project: ['./tsconfig.node.json', './tsconfig.app.json'],\n      tsconfigRootDir: import.meta.dirname,\n    },\n  },\n})\n```\n\n- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`\n- Optionally add `...tseslint.configs.stylisticTypeChecked`\n- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:\n\n```js\n// eslint.config.js\nimport react from 'eslint-plugin-react'\n\nexport default tseslint.config({\n  // Set the react version\n  settings: { react: { version: '18.3' } },\n  plugins: {\n    // Add the react plugin\n    react,\n  },\n  rules: {\n    // other rules...\n    // Enable its recommended rules\n    ...react.configs.recommended.rules,\n    ...react.configs['jsx-runtime'].rules,\n  },\n})\n```\n"
  },
  {
    "path": "examples/simple-tabs-starter/eslint.config.js",
    "content": "import js from '@eslint/js'\nimport globals from 'globals'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport reactRefresh from 'eslint-plugin-react-refresh'\nimport tseslint from 'typescript-eslint'\n\nexport default tseslint.config(\n  { ignores: ['dist'] },\n  {\n    extends: [js.configs.recommended, ...tseslint.configs.recommended],\n    files: ['**/*.{ts,tsx}'],\n    languageOptions: {\n      ecmaVersion: 2020,\n      globals: globals.browser,\n    },\n    plugins: {\n      'react-hooks': reactHooks,\n      'react-refresh': reactRefresh,\n    },\n    rules: {\n      ...reactHooks.configs.recommended.rules,\n      'react-refresh/only-export-components': [\n        'warn',\n        { allowConstantExport: true },\n      ],\n    },\n  },\n)\n"
  },
  {
    "path": "examples/simple-tabs-starter/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/vite.svg\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Vite + React + TS</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/simple-tabs-starter/package.json",
    "content": "{\n    \"name\": \"simple-tabs-starter\",\n    \"private\": true,\n    \"version\": \"0.0.0\",\n    \"type\": \"module\",\n    \"scripts\": {\n        \"dev\": \"vite\",\n        \"build\": \"tsc -b && vite build\",\n        \"lint\": \"eslint .\",\n        \"preview\": \"vite preview\"\n    },\n    \"dependencies\": {\n        \"keepalive-for-react\": \"^5.0.8\",\n        \"react\": \"^19.2.1\",\n        \"react-dom\": \"^19.2.1\",\n        \"zustand\": \"^5.0.9\"\n    },\n    \"devDependencies\": {\n        \"@eslint/js\": \"^9.39.1\",\n        \"@types/react\": \"^19.2.7\",\n        \"@types/react-dom\": \"^19.2.3\",\n        \"@vitejs/plugin-react\": \"^4.7.0\",\n        \"autoprefixer\": \"^10.4.22\",\n        \"eslint\": \"^9.39.1\",\n        \"eslint-plugin-react-hooks\": \"^5.2.0\",\n        \"eslint-plugin-react-refresh\": \"^0.4.24\",\n        \"globals\": \"^15.15.0\",\n        \"postcss\": \"^8.5.6\",\n        \"tailwindcss\": \"^3.4.18\",\n        \"typescript\": \"^5.9.3\",\n        \"typescript-eslint\": \"^8.48.1\",\n        \"vite\": \"^7.2.6\"\n    }\n}\n"
  },
  {
    "path": "examples/simple-tabs-starter/pnpm-workspace.yaml",
    "content": "# 这是一个独立项目，不属于父级 monorepo\npackages: []\n\n"
  },
  {
    "path": "examples/simple-tabs-starter/postcss.config.js",
    "content": "export default {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}\n"
  },
  {
    "path": "examples/simple-tabs-starter/src/App.tsx",
    "content": "import { useEffect, useMemo, useState } from \"react\";\nimport { useEffectOnActive, useKeepAliveContext, KeepAlive } from \"keepalive-for-react\";\nimport useCounterStore from \"./store/counter-store\";\n\nconst tabs = [\n    {\n        key: \"tab1\",\n        label: \"Tab 1\",\n        component: Tab1,\n    },\n    {\n        key: \"tab2\",\n        label: \"Tab 2\",\n        component: Tab2,\n    },\n    {\n        key: \"tab3\",\n        label: \"Tab 3\",\n        component: Tab3,\n    },\n];\n\nfunction App() {\n    const [currentTab, setCurrentTab] = useState<string>(\"tab1\");\n\n    const tab = useMemo(() => {\n        return tabs.find(tab => tab.key === currentTab);\n    }, [currentTab]);\n\n    const activeClass = \"tab-item cursor-pointer text-blue-500\";\n    const inactiveClass = \"tab-item cursor-pointer\";\n\n    return (\n        <div className=\"text-neutral-700 text-[16px] overflow-hidden max-w-[600px] mx-auto my-[20px] border border-neutral-200 rounded-md\">\n            <div className=\"tabs bg-gray-50 h-[40px] w-full flex gap-2 justify-around items-center\">\n                {tabs.map(tab => (\n                    <div\n                        key={tab.key}\n                        className={currentTab == tab.key ? activeClass : inactiveClass}\n                        onClick={() => setCurrentTab(tab.key)}\n                    >\n                        {tab.label}\n                    </div>\n                ))}\n            </div>\n            <div className=\"tab-content min-h-[200px]\">\n                {/*  虽然 Activity 可以提高性能，但是这里为了有淡出效果，禁用了19.2 的 Activity 组件 */}\n                <KeepAlive enableActivity={false} transition={true} activeCacheKey={currentTab} exclude={[\"tab3\"]}>\n                    {tab && <tab.component />}\n                </KeepAlive>\n            </div>\n        </div>\n    );\n}\n\nfunction Tab1() {\n    const [text, setText] = useState(\"Hello KeepAlive for React\");\n    const { refresh } = useKeepAliveContext();\n    const { count, increment, decrement } = useCounterStore();\n\n    useEffect(() => {\n        console.log(\"Tab1 count\", count);\n    }, [count]);\n    return (\n        <div className=\"p-[20px]\">\n            <div className=\"text-center text-lg font-bold\">Tab1</div>\n            <p className=\"text-center text-neutral-500 text-sm mt-[10px]\">\n                This is a demo for keepalive-for-react,\n                <br /> you can use it to keep the component alive when switching tabs.\n            </p>\n            <textarea\n                value={text}\n                onChange={e => {\n                    setText(e.target.value);\n                }}\n                className=\"w-full h-[100px] border border-neutral-200 rounded-md p-[10px] mt-[10px]\"\n            ></textarea>\n\n            <div className=\"buttons text-[14px] flex gap-2 justify-center mt-[10px]\">\n                <button onClick={() => refresh()} className=\"btn bg-green-500 active:bg-green-600 px-4 py-2 rounded-md text-white\">\n                    Reset\n                </button>\n                <button onClick={() => setText(\"\")} className=\"btn bg-red-500 active:bg-red-600 px-4 py-2 rounded-md text-white\">\n                    Clear\n                </button>\n            </div>\n            {/* shared counter */}\n            <div className=\"text-center text-lg font-bold my-[10px]\">Shared Counter</div>\n            <div className=\"text-center text-xl py-[10px] rounded-md bg-neutral-50 p-[10px]\">{count}</div>\n            <div className=\"flex gap-4 justify-center mt-[20px] text-[14px] mb-[20px]\">\n                <button onClick={() => increment()} className=\"btn bg-blue-500 active:bg-blue-600 px-4 py-2 rounded-md text-white\">\n                    Increase\n                </button>\n                <button onClick={() => decrement()} className=\"btn bg-red-500 active:bg-red-600 px-4 py-2 rounded-md text-white\">\n                    Decrease\n                </button>\n            </div>\n            <div className=\" mt-[20px] text-[12px] text-center\">\n                {\"Github: \"}\n                <a className=\"text-gray-500\" target=\"_blank\" href=\"https://github.com/finedaybreak/keepalive-for-react\">\n                    {\"https://github.com/finedaybreak/keepalive-for-react\"}\n                </a>\n            </div>\n        </div>\n    );\n}\n\nfunction Tab2() {\n    const [count, setCount] = useState(0);\n    const { refresh, active } = useKeepAliveContext();\n    const { count: sharedCount, increment: sharedIncrement, decrement: sharedDecrement } = useCounterStore();\n\n    useEffectOnActive(() => {\n        console.log(\"Tab2 Counter is active\", count);\n    }, [count]);\n\n    useEffect(() => {\n        console.log(\"Tab2 sharedCount\", sharedCount);\n    }, [sharedCount]);\n    return (\n        <div className=\"p-[20px]\">\n            <div className=\"text-center text-lg font-bold mb-[10px]\">Tab2</div>\n            <div className=\"status text-[12px] mb-[20px] text-center text-white \">\n                <span className={active ? \"bg-green-600 rounded-md px-[5px] py-[2px]\" : \"bg-red-500 rounded-md px-[5px] py-[2px]\"}>\n                    Active: {active ? \"true\" : \"false\"}\n                </span>\n            </div>\n            <div className=\"text-center text-xl py-[10px] rounded-md bg-neutral-50 p-[10px]\">{count}</div>\n            <div className=\"flex gap-4 justify-center mt-[20px] text-[14px]\">\n                <button\n                    className=\"px-[10px] py-[5px] bg-blue-500 active:bg-blue-600 text-white rounded-md\"\n                    onClick={() => setCount(count + 1)}\n                >\n                    Increase\n                </button>\n                <button\n                    className=\"px-[10px] py-[5px] bg-pink-500 active:bg-pink-600 text-white rounded-md\"\n                    onClick={() => setCount(count - 1)}\n                >\n                    Decrease\n                </button>\n                <button className=\"px-[10px] py-[5px] bg-green-500 active:bg-green-600 text-white rounded-md\" onClick={() => refresh()}>\n                    Refresh\n                </button>\n            </div>\n            {/* shared counter */}\n            <div className=\"text-center text-lg font-bold my-[10px]\">Shared Counter</div>\n            <div className=\"text-center text-xl py-[10px] rounded-md bg-neutral-50 p-[10px]\">{sharedCount}</div>\n            <div className=\"flex gap-4 justify-center mt-[20px] text-[14px] mb-[20px]\">\n                <button onClick={() => sharedIncrement()} className=\"btn bg-blue-500 active:bg-blue-600 px-4 py-2 rounded-md text-white\">\n                    Increase\n                </button>\n                <button onClick={() => sharedDecrement()} className=\"btn bg-red-500 active:bg-red-600 px-4 py-2 rounded-md text-white\">\n                    Decrease\n                </button>\n            </div>\n        </div>\n    );\n}\n\nfunction Tab3() {\n    const [count, setCount] = useState(0);\n    const { refresh, active } = useKeepAliveContext();\n\n    useEffectOnActive(() => {\n        console.log(\"Tab3 Counter is active\", count, active);\n    }, [count]);\n    return (\n        <div className=\"p-[20px]\">\n            <div className=\"text-center text-lg font-bold mb-[10px]\">Tab3 (no cache)</div>\n            <div className=\"text-center text-xl py-[10px] rounded-md bg-neutral-50 p-[10px]\">{count}</div>\n            <div className=\"flex gap-4 justify-center mt-[20px] text-[14px]\">\n                <button\n                    className=\"px-[10px] py-[5px] bg-blue-500 active:bg-blue-600 text-white rounded-md\"\n                    onClick={() => setCount(count + 1)}\n                >\n                    Increase\n                </button>\n                <button\n                    className=\"px-[10px] py-[5px] bg-pink-500 active:bg-pink-600 text-white rounded-md\"\n                    onClick={() => setCount(count - 1)}\n                >\n                    Decrease\n                </button>\n                <button className=\"px-[10px] py-[5px] bg-green-500 active:bg-green-600 text-white rounded-md\" onClick={() => refresh()}>\n                    Refresh\n                </button>\n            </div>\n        </div>\n    );\n}\n\nexport default App;\n"
  },
  {
    "path": "examples/simple-tabs-starter/src/index.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n/* \nkeepalive-for-react animation example\ntransition should be set true to enable animation\nand duration should be set to the animation duration\n*/\n\n.cache-component.active {\n    animation:\n        fadeIn 0.3s ease-in-out,\n        slideIn 0.3s ease-in-out;\n}\n\n.cache-component.inactive {\n    animation:\n        fadeOut 0.3s ease-in-out,\n        slideOut 0.3s ease-in-out;\n}\n\n@keyframes fadeIn {\n    from {\n        opacity: 0;\n    }\n    to {\n        opacity: 1;\n    }\n}\n\n@keyframes fadeOut {\n    from {\n        opacity: 1;\n    }\n    to {\n        opacity: 0;\n    }\n}\n\n@keyframes slideIn {\n    from {\n        transform: translateX(-100%);\n    }\n    to {\n        transform: translateX(0);\n    }\n}\n\n@keyframes slideOut {\n    from {\n        transform: translateX(0);\n    }\n    to {\n        transform: translateX(100%);\n    }\n}\n"
  },
  {
    "path": "examples/simple-tabs-starter/src/main.tsx",
    "content": "import { createRoot } from \"react-dom/client\";\nimport App from \"./App.tsx\";\nimport \"./index.css\";\n\ncreateRoot(document.getElementById(\"root\")!).render(<App />);\n"
  },
  {
    "path": "examples/simple-tabs-starter/src/store/counter-store.tsx",
    "content": "import { create } from \"zustand\";\n\nexport const useCounterStore = create<CounterStore>(set => ({\n    count: 0,\n    increment: () => set(state => ({ count: state.count + 1 })),\n    decrement: () => set(state => ({ count: state.count - 1 })),\n}));\n\ninterface CounterStore {\n    count: number;\n    increment: () => void;\n    decrement: () => void;\n}\n\nexport default useCounterStore;\n"
  },
  {
    "path": "examples/simple-tabs-starter/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/simple-tabs-starter/tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\nexport default {\n  content: [\n    \"./index.html\",\n    \"./src/**/*.{js,ts,jsx,tsx}\",\n  ],\n  theme: {\n    extend: {},\n  },\n  plugins: [],\n}\n\n"
  },
  {
    "path": "examples/simple-tabs-starter/tsconfig.app.json",
    "content": "{\n    \"compilerOptions\": {\n        \"target\": \"ES2020\",\n        \"useDefineForClassFields\": true,\n        \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n        \"module\": \"ESNext\",\n        \"skipLibCheck\": true,\n\n        /* Bundler mode */\n        \"moduleResolution\": \"bundler\",\n        \"allowImportingTsExtensions\": true,\n        \"isolatedModules\": true,\n        \"moduleDetection\": \"force\",\n        \"noEmit\": true,\n        \"jsx\": \"react-jsx\",\n\n        /* Linting */\n        \"strict\": true,\n        \"noUnusedLocals\": true,\n        \"noUnusedParameters\": true,\n        \"noFallthroughCasesInSwitch\": true\n    },\n    \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "examples/simple-tabs-starter/tsconfig.app.tsbuildinfo",
    "content": "{\"root\":[\"./src/app.tsx\",\"./src/main.tsx\",\"./src/vite-env.d.ts\",\"./src/store/counter-store.tsx\"],\"version\":\"5.9.3\"}"
  },
  {
    "path": "examples/simple-tabs-starter/tsconfig.json",
    "content": "{\n    \"files\": [],\n    \"references\": [{ \"path\": \"./tsconfig.app.json\" }, { \"path\": \"./tsconfig.node.json\" }]\n}\n"
  },
  {
    "path": "examples/simple-tabs-starter/tsconfig.node.json",
    "content": "{\n    \"compilerOptions\": {\n        \"target\": \"ES2022\",\n        \"lib\": [\"ES2023\"],\n        \"module\": \"ESNext\",\n        \"skipLibCheck\": true,\n\n        /* Bundler mode */\n        \"moduleResolution\": \"bundler\",\n        \"allowImportingTsExtensions\": true,\n        \"isolatedModules\": true,\n        \"moduleDetection\": \"force\",\n        \"noEmit\": true,\n\n        /* Linting */\n        \"strict\": true,\n        \"noUnusedLocals\": true,\n        \"noUnusedParameters\": true,\n        \"noFallthroughCasesInSwitch\": true\n    },\n    \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "examples/simple-tabs-starter/tsconfig.node.tsbuildinfo",
    "content": "{\"root\":[\"./vite.config.ts\"],\"version\":\"5.9.3\"}"
  },
  {
    "path": "examples/simple-tabs-starter/vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n    plugins: [react()],\n});\n"
  },
  {
    "path": "package.json",
    "content": "{\n    \"name\": \"keepalive-for-react-monorepo\",\n    \"version\": \"0.0.0\",\n    \"private\": true,\n    \"description\": \"A react <KeepAlive/> component like <keep-alive/> in vue\",\n    \"type\": \"module\",\n    \"scripts\": {\n        \"build\": \"pnpm -r build\",\n        \"clean\": \"pnpm -r clean\",\n        \"format\": \"prettier --write \\\"**/*.{ts,tsx,json}\\\"\",\n        \"changeset\": \"changeset\",\n        \"version\": \"changeset version\",\n        \"release\": \"pnpm build && changeset publish\",\n        \"prepare\": \"husky\",\n        \"example:router\": \"cd examples/react-router-dom-simple-starter && pnpm install && pnpm dev\",\n        \"example:tabs\": \"cd examples/simple-tabs-starter && pnpm install && pnpm dev\"\n    },\n    \"lint-staged\": {\n        \"*.{ts,tsx,json,md}\": \"prettier --write\"\n    },\n    \"author\": \"wongyichen\",\n    \"license\": \"MIT\",\n    \"devDependencies\": {\n        \"@changesets/cli\": \"^2.30.0\",\n        \"@types/node\": \"^20.8.2\",\n        \"@types/react\": \"^19.0.0\",\n        \"@types/react-dom\": \"^19.0.0\",\n        \"husky\": \"^9.0.11\",\n        \"lint-staged\": \"^15.2.2\",\n        \"prettier\": \"^3.0.3\",\n        \"tsup\": \"^8.5.1\",\n        \"typescript\": \"^5.2.2\"\n    },\n    \"engines\": {\n        \"node\": \">=18\",\n        \"pnpm\": \">=8\"\n    },\n    \"packageManager\": \"pnpm@10.24.0\",\n    \"pnpm\": {\n        \"onlyBuiltDependencies\": [\n            \"esbuild\"\n        ]\n    }\n}\n"
  },
  {
    "path": "packages/core/CHANGELOG.md",
    "content": "# keepalive-for-react\n\n## 5.0.11\n\n### Patch Changes\n\n-   fix destroy emit\n\n## 5.0.10\n\n### Patch Changes\n\n-   new hooks: useEffectOnCreate, useLayoutEffectOnCreate\n\n## 5.0.9\n\n### Patch Changes\n\n-   Wraps Activity with a delayed hide so the outgoing component stays visible during the transition duration, preserving fade/transition effects when enableActivity is on.\n\n## 5.0.8\n\n### Patch Changes\n\n-   fix(CacheComponent): switch useEffect to useLayoutEffect for DOM operations\n\n## 5.0.7\n\n### Patch Changes\n\n-   change enableActivity to default false\n\n## 5.0.6\n\n### Patch Changes\n\n-   update doc\n\n## 5.0.4\n\n### Patch Changes\n\n-   bug fixes\n-   bux fix\n-   bug fixes\n-   bug fixes\n-   bug fix\n-   Bug fixes\n-   bug fixes\n-   bug fixes\n-   bug fixes\n\n## 5.0.4-beta.8\n\n### Patch Changes\n\n-   bug fixes\n\n## 5.0.4-beta.7\n\n### Patch Changes\n\n-   bug fixes\n\n## 5.0.4-beta.6\n\n### Patch Changes\n\n-   bug fixes\n\n## 5.0.4-beta.5\n\n### Patch Changes\n\n-   bug fixes\n\n## 5.0.4-beta.4\n\n### Patch Changes\n\n-   bug fixes\n\n## 5.0.4-beta.3\n\n### Patch Changes\n\n-   bug fixes\n\n## 5.0.4-beta.1\n\n### Patch Changes\n\n-   bug fixes\n\n## 5.0.4-beta.0\n\n### Patch Changes\n\n-   Bug fixes\n"
  },
  {
    "path": "packages/core/README.md",
    "content": "<p align=\"center\">\n  <img width=\"120\" src=\"./react-keepalive.png\" alt=\"keepalive-for-react logo\">\n</p>\n\n<div align=\"center\">\n  <h1 align=\"center\">\n    KeepAlive for React\n  </h1>\n</div>\n\n<p align=\"center\">A React KeepAlive component like keep-alive in vue</p>\n\n[中文](./README.zh_CN.md) | English\n\n[![NPM version](https://img.shields.io/npm/v/keepalive-for-react.svg?style=flat)](https://npmjs.com/package/keepalive-for-react) [![NPM downloads](https://img.shields.io/npm/dm/keepalive-for-react.svg?style=flat)](https://npmjs.com/package/keepalive-for-react) [![][discord-shield]][discord-link]<br/>\n\n## Packages\n\n| Package                                         | Version                                                                                                                                        | Description                  |\n| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |\n| [keepalive-for-react](./packages/core)          | [![NPM version](https://img.shields.io/npm/v/keepalive-for-react.svg?style=flat)](https://npmjs.com/package/keepalive-for-react)               | Core keepalive functionality |\n| [keepalive-for-react-router](./packages/router) | [![NPM version](https://img.shields.io/npm/v/keepalive-for-react-router.svg?style=flat)](https://npmjs.com/package/keepalive-for-react-router) | React Router integration     |\n\n## Features\n\n-   Support react-router-dom v6+ or react-router v7+\n-   Support React v16+ ~ v18+ (v19.2 Activity component support [v5.0.0])\n-   Support Suspense and Lazy import\n-   Support ErrorBoundary\n-   Support Custom Container\n-   Support Switching Animation Transition with className `active` and `inactive`\n-   Simply implement, without any extra dependencies and hacking ways\n-   Only 6KB minified size\n-   Support interrupt state effect when component is not active (v5.0.0)\n\n## Attention\n\n-   **Version Compatibility**:\n\n    -   For React 18, please use `keepalive-for-react@4.x.x`\n    -   For React 19.2+, please use `keepalive-for-react@5.x.x`\n\n-   DO NOT use <React.StrictMode />, it CANNOT work with keepalive-for-react in development mode. because it can lead to\n    some unexpected behavior.\n\n-   In Router only support react-router-dom v6+\n\n## Install\n\n```bash\nnpm install keepalive-for-react\n```\n\n```bash\nyarn add keepalive-for-react\n```\n\n```bash\npnpm add keepalive-for-react\n```\n\n## Usage\n\n### in react-router-dom v6+ or react-router v7+\n\n1. install react-router-dom v6+ or react-router v7+\n\n```bash\n# v6+\nnpm install react-router-dom keepalive-for-react keepalive-for-react-router@1.x.x\n# v7+\nnpm install react-router keepalive-for-react keepalive-for-react-router@2.x.x\n```\n\n2. use KeepAlive in your project\n\n```tsx\n// v6+ keepalive-for-react-router@1.x.x\n// v7+ keepalive-for-react-router@2.x.x\nimport KeepAliveRouteOutlet from \"keepalive-for-react-router\";\n\nfunction Layout() {\n    return (\n        <div className=\"layout\">\n            <KeepAliveRouteOutlet />\n        </div>\n    );\n}\n```\n\nor\n\n```tsx\nimport { useMemo } from \"react\";\n// v6+\nimport { useLocation, useOutlet } from \"react-router-dom\";\n// v7\n// import { useLocation, useOutlet } from \"react-router\";\nimport { KeepAlive, useKeepAliveRef } from \"keepalive-for-react\";\n\nfunction Layout() {\n    const location = useLocation();\n    const aliveRef = useKeepAliveRef();\n\n    const outlet = useOutlet();\n\n    // determine which route component to is active\n    const currentCacheKey = useMemo(() => {\n        return location.pathname + location.search;\n    }, [location.pathname, location.search]);\n\n    return (\n        <div className=\"layout\">\n            <MemoizedScrollTop>\n                <KeepAlive transition aliveRef={aliveRef} activeCacheKey={currentCacheKey} max={18}>\n                    <Suspense fallback={<LoadingArea />}>\n                        <SpreadArea>{outlet}</SpreadArea>\n                    </Suspense>\n                </KeepAlive>\n            </MemoizedScrollTop>\n        </div>\n    );\n}\n```\n\ndetails see [examples/react-router-dom-simple-starter](./examples/react-router-dom-simple-starter)\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/finedaybreak/keepalive-for-react/tree/main/examples/react-router-dom-simple-starter)\n\n### in simple tabs\n\n```bash\nnpm install keepalive-for-react\n```\n\n```tsx\nconst tabs = [\n    {\n        key: \"tab1\",\n        label: \"Tab 1\",\n        component: Tab1,\n    },\n    {\n        key: \"tab2\",\n        label: \"Tab 2\",\n        component: Tab2,\n    },\n    {\n        key: \"tab3\",\n        label: \"Tab 3\",\n        component: Tab3,\n    },\n];\n\nfunction App() {\n    const [currentTab, setCurrentTab] = useState<string>(\"tab1\");\n\n    const tab = useMemo(() => {\n        return tabs.find(tab => tab.key === currentTab);\n    }, [currentTab]);\n\n    return (\n        <div>\n            {/* ... */}\n            <KeepAlive transition={true} activeCacheKey={currentTab} exclude={[\"tab3\"]}>\n                {tab && <tab.component />}\n            </KeepAlive>\n        </div>\n    );\n}\n```\n\ndetails see [examples/simple-tabs-starter](./examples/simple-tabs-starter)\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/finedaybreak/keepalive-for-react/tree/main/examples/simple-tabs-starter)\n\n## KeepAlive Props\n\ntype definition\n\n```tsx\ninterface KeepAliveProps {\n    // determine which component to is active\n    activeCacheKey: string;\n    children?: KeepAliveChildren;\n    /**\n     * max cache count default 10\n     */\n    max?: number;\n    exclude?: Array<string | RegExp> | string | RegExp;\n    include?: Array<string | RegExp> | string | RegExp;\n    onBeforeActive?: (activeCacheKey: string) => void;\n    customContainerRef?: RefObject<HTMLDivElement>;\n    cacheNodeClassName?: string;\n    containerClassName?: string;\n    errorElement?: ComponentType<{\n        children: ReactNode;\n    }>;\n    /**\n     * transition default false\n     */\n    transition?: boolean;\n    /**\n     * use view transition to animate the component when switching tabs\n     * @see https://developer.chrome.com/docs/web-platform/view-transitions/\n     */\n    viewTransition?: boolean;\n    /**\n     * transition duration default 200\n     */\n    duration?: number;\n    aliveRef?: RefObject<KeepAliveRef | undefined>;\n    /**\n     * max alive time for cache node (second)\n     * @default 0 (no limit)\n     */\n    maxAliveTime?: number | MaxAliveConfig[];\n    /**\n     * enable Activity component from react 19+\n     * @default false\n     * Activity component can improve performance\n     * Attention: if enable Activity component, useEffect will trigger when the component is active\n     */\n    enableActivity?: boolean;\n}\n\ninterface MaxAliveConfig {\n    match: string | RegExp;\n    expire: number;\n}\n```\n\n## Hooks\n\n### useEffectOnActive\n\n```tsx\nuseEffectOnActive(() => {\n    console.log(\"active\");\n}, []);\n```\n\n### useLayoutEffectOnActive\n\n```tsx\nuseLayoutEffectOnActive(\n    () => {\n        console.log(\"active\");\n    },\n    [],\n    false,\n);\n// the third parameter is optional, default is false,\n// if true, which means the callback will be skipped when the useLayoutEffect is triggered in first render\n```\n\n### useEffectOnCreate\n\nRun a callback only once when the component is first created (cached), and run the returned cleanup only when the component is destroyed from the cache. Unlike `useEffect(fn, [])`, it will NOT re-run when the cached component is re-activated.\n\n```tsx\nuseEffectOnCreate(() => {\n    console.log(\"component created\");\n    return () => {\n        console.log(\"component destroyed\");\n    };\n});\n```\n\n### useLayoutEffectOnCreate\n\nSame as `useEffectOnCreate` but uses `useLayoutEffect` internally. Useful when the create-time logic needs to run synchronously before the browser paints.\n\n```tsx\nuseLayoutEffectOnCreate(() => {\n    console.log(\"component created (layout)\");\n    return () => {\n        console.log(\"component destroyed (layout)\");\n    };\n});\n```\n\n### useKeepAliveContext\n\ntype definition\n\n```ts\ninterface KeepAliveContext {\n    /**\n     * whether the component is active\n     */\n    active: boolean;\n    /**\n     * refresh the component\n     * @param {string} [cacheKey] - The cache key of the component. If not provided, the current cached component will be refreshed.\n     */\n    refresh: (cacheKey?: string) => void;\n    /**\n     * destroy the component\n     * @param {string} [cacheKey] - the cache key of the component, if not provided, current active cached component will be destroyed\n     */\n    destroy: (cacheKey?: string | string[]) => Promise<void>;\n    /**\n     * destroy all components\n     */\n    destroyAll: () => Promise<void>;\n    /**\n     * destroy other components except the provided cacheKey\n     * @param {string} [cacheKey] - The cache key of the component. If not provided, destroy all components except the current active cached component.\n     */\n    destroyOther: (cacheKey?: string) => Promise<void>;\n    /**\n     * get the cache nodes\n     */\n    getCacheNodes: () => Array<CacheNode>;\n}\n```\n\n```tsx\nconst { active, refresh, destroy, getCacheNodes } = useKeepAliveContext();\n// active is a boolean, true is active, false is inactive\n// refresh is a function, you can call it to refresh the component\n// destroy is a function, you can call it to destroy the component\n// ...\n// getCacheNodes is a function, you can call it to get the cache nodes\n```\n\n### useKeepAliveRef\n\ntype definition\n\n```ts\ninterface KeepAliveRef {\n    refresh: (cacheKey?: string) => void;\n    destroy: (cacheKey?: string | string[]) => Promise<void>;\n    destroyAll: () => Promise<void>;\n    destroyOther: (cacheKey?: string) => Promise<void>;\n    getCacheNodes: () => Array<CacheNode>;\n}\n```\n\n```tsx\nfunction App() {\n    const aliveRef = useKeepAliveRef();\n    // aliveRef.current is a KeepAliveRef object\n\n    // you can call refresh and destroy on aliveRef.current\n    aliveRef.current?.refresh();\n    // it is not necessary to call destroy manually, KeepAlive will handle it automatically\n    aliveRef.current?.destroy();\n\n    return <KeepAlive aliveRef={aliveRef}>{/* ... */}</KeepAlive>;\n}\n// or\nfunction AppRouter() {\n    const aliveRef = useKeepAliveRef();\n    // aliveRef.current is a KeepAliveRef object\n\n    // you can call refresh and destroy on aliveRef.current\n    aliveRef.current?.refresh();\n    aliveRef.current?.destroy();\n    return <KeepAliveRouteOutlet aliveRef={aliveRef} />;\n}\n```\n\n## Development\n\ninstall dependencies\n\n```bash\npnpm install\n```\n\nbuild package\n\n```bash\npnpm build\n```\n\n[discord-link]: https://discord.gg/ycf896w7eA\n[discord-shield]: https://img.shields.io/discord/1232158668913381467?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square\n[discord-shield-badge]: https://img.shields.io/discord/1232158668913381467?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=for-the-badge\n"
  },
  {
    "path": "packages/core/README.zh_CN.md",
    "content": "<p align=\"center\">\n  <img width=\"120\" src=\"./react-keepalive.png\" alt=\"keepalive-for-react logo\">\n</p>\n\n<div align=\"center\">\n  <h1 align=\"center\">\n    React KeepAlive 组件\n  </h1>\n</div>\n\n<p align=\"center\">一个类似Vue中keep-alive的React KeepAlive组件</p>\n\n[English](./README.md) | 中文\n\n[![NPM版本](https://img.shields.io/npm/v/keepalive-for-react.svg?style=flat)](https://npmjs.com/package/keepalive-for-react) [![NPM下载量](https://img.shields.io/npm/dm/keepalive-for-react.svg?style=flat)](https://npmjs.com/package/keepalive-for-react)\n\n## 包信息\n\n| 包名                                            | 版本                                                                                                                                       | 描述              |\n| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- |\n| [keepalive-for-react](./packages/core)          | [![NPM版本](https://img.shields.io/npm/v/keepalive-for-react.svg?style=flat)](https://npmjs.com/package/keepalive-for-react)               | 核心keepalive功能 |\n| [keepalive-for-react-router](./packages/router) | [![NPM版本](https://img.shields.io/npm/v/keepalive-for-react-router.svg?style=flat)](https://npmjs.com/package/keepalive-for-react-router) | React Router集成  |\n\n## 特性\n\n-   支持react-router-dom v6+ 或 react-router v7+\n-   支持React v16+ ~ v18+ (v19.2 Activity component support [v5.0.0])\n-   支持Suspense和懒加载导入\n-   支持错误边界\n-   支持自定义容器\n-   支持使用className `active`和`inactive`进行切换动画过渡\n-   简单实现,无需任何额外依赖和hack方式\n-   压缩后仅6KB大小\n-   支持中断state Effect当组件不活动时 (v5.0.0)\n\n## 注意事项\n\n-   **版本兼容性**：\n\n    -   React 18 请使用 `keepalive-for-react@4.x.x`\n    -   React 19.2+ 请使用 `keepalive-for-react@5.x.x`\n\n-   请勿使用 <React.StrictMode />,它在开发模式下无法与keepalive-for-react一起工作。因为它可能会导致一些意外行为。\n\n-   在路由中仅支持react-router-dom v6+\n\n## 安装\n\n```bash\nnpm install keepalive-for-react\n```\n\n```bash\nyarn add keepalive-for-react\n```\n\n```bash\npnpm add keepalive-for-react\n```\n\n## 使用\n\n### 配合react-router-dom v6+ 或 react-router v7+使用\n\n1. 安装react-router-dom v6+ 或 react-router v7+\n\n```bash\n# v6+\nnpm install react-router-dom keepalive-for-react keepalive-for-react-router@1.x.x\n# v7+\nnpm install react-router keepalive-for-react keepalive-for-react-router@2.x.x\n```\n\n2. 在项目中使用KeepAlive\n\n```tsx\n// v6+ keepalive-for-react-router@1.x.x\n// v7+ keepalive-for-react-router@2.x.x\nimport KeepAliveRouteOutlet from \"keepalive-for-react-router\";\n\nfunction Layout() {\n    return (\n        <div className=\"layout\">\n            <KeepAliveRouteOutlet />\n        </div>\n    );\n}\n```\n\n或者\n\n```tsx\nimport { useMemo } from \"react\";\nimport { useLocation } from \"react-router-dom\";\nimport { KeepAlive, useKeepAliveRef } from \"keepalive-for-react\";\n\nfunction Layout() {\n    const location = useLocation();\n    const aliveRef = useKeepAliveRef();\n\n    const outlet = useOutlet();\n\n    // 确定哪个路由组件处于活动状态\n    const currentCacheKey = useMemo(() => {\n        return location.pathname + location.search;\n    }, [location.pathname, location.search]);\n\n    return (\n        <div className=\"layout\">\n            <MemoizedScrollTop>\n                <KeepAlive transition aliveRef={aliveRef} activeCacheKey={currentCacheKey} max={18}>\n                    <Suspense fallback={<LoadingArea />}>\n                        <SpreadArea>{outlet}</SpreadArea>\n                    </Suspense>\n                </KeepAlive>\n            </MemoizedScrollTop>\n        </div>\n    );\n}\n```\n\n详情请参见 [examples/react-router-dom-simple-starter](./examples/react-router-dom-simple-starter)\n\n[![在StackBlitz中打开](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/finedaybreak/keepalive-for-react/tree/main/examples/react-router-dom-simple-starter)\n\n### 在简单标签页中\n\n```bash\nnpm install keepalive-for-react\n```\n\n```tsx\nconst tabs = [\n    {\n        key: \"tab1\",\n        label: \"标签1\",\n        component: Tab1,\n    },\n    {\n        key: \"tab2\",\n        label: \"标签2\",\n        component: Tab2,\n    },\n    {\n        key: \"tab3\",\n        label: \"标签3\",\n        component: Tab3,\n    },\n];\n\nfunction App() {\n    const [currentTab, setCurrentTab] = useState<string>(\"tab1\");\n\n    const tab = useMemo(() => {\n        return tabs.find(tab => tab.key === currentTab);\n    }, [currentTab]);\n\n    return (\n        <div>\n            {/* ... */}\n            <KeepAlive transition={true} activeCacheKey={currentTab} exclude={[\"tab3\"]}>\n                {tab && <tab.component />}\n            </KeepAlive>\n        </div>\n    );\n}\n```\n\n详情请参见 [examples/simple-tabs-starter](./examples/simple-tabs-starter)\n\n[![在StackBlitz中打开](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/finedaybreak/keepalive-for-react/tree/main/examples/simple-tabs-starter)\n\n## KeepAlive 属性\n\n类型定义\n\n```tsx\ninterface KeepAliveProps {\n    // 确定哪个组件处于活动状态\n    activeCacheKey: string;\n    children?: KeepAliveChildren;\n    /**\n     * 最大缓存数量 默认10\n     */\n    max?: number;\n    exclude?: Array<string | RegExp> | string | RegExp;\n    include?: Array<string | RegExp> | string | RegExp;\n    onBeforeActive?: (activeCacheKey: string) => void;\n    customContainerRef?: RefObject<HTMLDivElement>;\n    cacheNodeClassName?: string;\n    containerClassName?: string;\n    errorElement?: ComponentType<{\n        children: ReactNode;\n    }>;\n    /**\n     * 过渡效果 默认false\n     */\n    transition?: boolean;\n\n    /**\n     * 使用view transition来过渡组件 默认false\n     * @see https://developer.chrome.com/docs/web-platform/view-transitions/\n     */\n    viewTransition?: boolean;\n\n    /**\n     * 过渡时间 默认200ms\n     */\n    duration?: number;\n    aliveRef?: RefObject<KeepAliveRef | undefined>;\n    /**\n     * 缓存节点最大存活时间 (秒)\n     * @default 0 (无限制)\n     */\n    maxAliveTime?: number | MaxAliveConfig[];\n    /**\n     * enable Activity component from react 19+\n     * @default false\n     * Activity component can improve performance\n     * Attention: if enable Activity component, useEffect will trigger when the component is active\n     */\n    enableActivity?: boolean;\n}\n\ninterface MaxAliveConfig {\n    match: string | RegExp;\n    expire: number;\n}\n```\n\n## Hooks\n\n### useEffectOnActive\n\n```tsx\nuseEffectOnActive(() => {\n    console.log(\"active\");\n}, []);\n```\n\n### useLayoutEffectOnActive\n\n```tsx\nuseLayoutEffectOnActive(\n    () => {\n        console.log(\"active\");\n    },\n    [],\n    false,\n);\n// 第三个参数是可选的,默认为false,\n// 如果为true,表示在首次渲染时触发useLayoutEffect时会跳过回调\n```\n\n### useEffectOnCreate\n\n只在组件首次创建(加入缓存)时执行一次回调，并在组件从缓存中被销毁时执行返回的清理函数。与 `useEffect(fn, [])` 不同，被缓存的组件再次激活时 **不会** 重新执行。\n\n```tsx\nuseEffectOnCreate(() => {\n    console.log(\"组件创建\");\n    return () => {\n        console.log(\"组件销毁\");\n    };\n});\n```\n\n### useLayoutEffectOnCreate\n\n与 `useEffectOnCreate` 行为一致,内部使用 `useLayoutEffect`。适用于需要在浏览器绘制前同步执行创建逻辑的场景。\n\n```tsx\nuseLayoutEffectOnCreate(() => {\n    console.log(\"组件创建 (layout)\");\n    return () => {\n        console.log(\"组件销毁 (layout)\");\n    };\n});\n```\n\n### useKeepAliveContext\n\n类型定义\n\n```ts\ninterface KeepAliveContext {\n    /**\n     * 组件是否处于活动状态\n     */\n    active: boolean;\n    /**\n     * 刷新组件\n     * @param {string} [cacheKey] - 组件的缓存键。如果未提供，将刷新当前缓存的组件。\n     */\n    refresh: (cacheKey?: string) => void;\n    /**\n     * 销毁组件\n     * @param {string} [cacheKey] - 组件的缓存键，如果未提供，将销毁当前活动的缓存组件。\n     */\n    destroy: (cacheKey?: string | string[]) => Promise<void>;\n    /**\n     * 销毁所有组件\n     */\n    destroyAll: () => Promise<void>;\n    /**\n     * 销毁除提供的cacheKey外的其他组件\n     * @param {string} [cacheKey] - 组件的缓存键。如果未提供，将销毁除当前活动缓存组件外的所有组件。\n     */\n    destroyOther: (cacheKey?: string) => Promise<void>;\n    /**\n     * 获取缓存节点\n     */\n    getCacheNodes: () => Array<CacheNode>;\n}\n```\n\n```tsx\nconst { active, refresh, destroy, getCacheNodes } = useKeepAliveContext();\n// active 是一个布尔值，true表示活动，false表示非活动\n// refresh 是一个函数，你可以调用它来刷新组件\n// destroy 是一个函数，你可以调用它来销毁组件\n// ...\n// getCacheNodes 是一个函数，你可以调用它来获取缓存节点\n```\n\n### useKeepAliveRef\n\n类型定义\n\n```ts\ninterface KeepAliveRef {\n    refresh: (cacheKey?: string) => void;\n    destroy: (cacheKey?: string | string[]) => Promise<void>;\n    destroyAll: () => Promise<void>;\n    destroyOther: (cacheKey?: string) => Promise<void>;\n    getCacheNodes: () => Array<CacheNode>;\n}\n```\n\n```tsx\nfunction App() {\n    const aliveRef = useKeepAliveRef();\n    // aliveRef.current 是一个 KeepAliveRef 对象\n\n    // 你可以在 aliveRef.current 上调用 refresh 和 destroy\n    aliveRef.current?.refresh();\n    // 通常不需要手动调用 destroy,KeepAlive 会自动处理\n    aliveRef.current?.destroy();\n\n    return <KeepAlive aliveRef={aliveRef}>{/* ... */}</KeepAlive>;\n}\n// 或者\nfunction AppRouter() {\n    const aliveRef = useKeepAliveRef();\n    // aliveRef.current 是一个 KeepAliveRef 对象\n\n    // 你可以在 aliveRef.current 上调用 refresh 和 destroy\n    aliveRef.current?.refresh();\n    aliveRef.current?.destroy();\n    return <KeepAliveRouteOutlet aliveRef={aliveRef} />;\n}\n```\n\n## 开发\n\n安装依赖\n\n```bash\npnpm install\n```\n\n构建包\n\n```bash\npnpm build\n```\n\n链接包到全局\n\n```bash\npnpm link --global\n```\n\n在演示项目中测试\n\n```bash\ncd demo\npnpm link --global keepalive-for-react\n```\n\n[discord-link]: https://discord.gg/ycf896w7eA\n[discord-shield]: https://img.shields.io/discord/1232158668913381467?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square\n[discord-shield-badge]: https://img.shields.io/discord/1232158668913381467?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=for-the-badge\n"
  },
  {
    "path": "packages/core/package.json",
    "content": "{\n    \"name\": \"keepalive-for-react\",\n    \"version\": \"5.0.11\",\n    \"description\": \"A react <KeepAlive/> component like <keep-alive/> in vue\",\n    \"homepage\": \"https://github.com/finedaybreak/keepalive-for-react\",\n    \"repository\": {\n        \"type\": \"git\",\n        \"url\": \"git+https://github.com/finedaybreak/keepalive-for-react.git\"\n    },\n    \"exports\": {\n        \".\": {\n            \"types\": \"./dist/index.d.ts\",\n            \"import\": \"./dist/index.mjs\",\n            \"require\": \"./dist/index.cjs\"\n        }\n    },\n    \"main\": \"./dist/index.cjs\",\n    \"module\": \"./dist/index.mjs\",\n    \"types\": \"./dist/index.d.ts\",\n    \"type\": \"module\",\n    \"files\": [\n        \"dist\"\n    ],\n    \"scripts\": {\n        \"build\": \"tsup\",\n        \"clean\": \"rm -rf dist\"\n    },\n    \"keywords\": [\n        \"keepalive\",\n        \"keep-alive\",\n        \"react keepalive\",\n        \"keepalive for react\",\n        \"keepalive-for-react\"\n    ],\n    \"author\": \"wongyichen\",\n    \"license\": \"MIT\",\n    \"peerDependencies\": {\n        \"react\": \">=16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0\",\n        \"react-dom\": \">=16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0\"\n    },\n    \"dependencies\": {\n        \"mitt\": \"^3.0.1\"\n    }\n}\n"
  },
  {
    "path": "packages/core/src/compat/Activity.tsx",
    "content": "import * as React from \"react\";\n\n// React 19+ 原生 Activity 组件\nconst NativeActivity = (React as any).Activity;\n\nexport interface ActivityProps {\n    children: React.ReactNode;\n    mode: \"visible\" | \"hidden\";\n}\n\n/**\n * Activity 兼容组件\n * - React 19+: 使用原生 Activity\n * - React 18: 使用 Fragment fallback（用户自行处理隐藏逻辑）\n */\nexport const Activity: React.ComponentType<ActivityProps> = NativeActivity ?? React.Fragment;\n\nexport const hasNativeActivity = !!NativeActivity;\n"
  },
  {
    "path": "packages/core/src/compat/safeStartTransition.ts",
    "content": "import { startTransition as reactStartTransition } from \"react\";\nimport { isFn } from \"../utils\";\n\n/**\n * Compatible with React versions < 18 startTransition\n * @param cb Callback function to be executed in transition\n */\nconst safeStartTransition = (cb: () => void): void => {\n    if (typeof reactStartTransition !== \"undefined\" && isFn(reactStartTransition)) {\n        reactStartTransition(cb);\n    } else {\n        cb();\n    }\n};\n\nexport default safeStartTransition;\n"
  },
  {
    "path": "packages/core/src/components/CacheComponent/index.tsx",
    "content": "import { ComponentType, Fragment, memo, ReactNode, RefObject, useLayoutEffect, useMemo, useRef } from \"react\";\nimport { hasNativeActivity } from \"../../compat/Activity\";\nimport { createPortal } from \"react-dom\";\nimport { delayAsync, domAttrSet, isInclude } from \"../../utils\";\nimport MemoizedActivty from \"../MemoizedActivty\";\n\nexport interface CacheComponentProps {\n    children: ReactNode;\n    errorElement?: ComponentType<{\n        children: ReactNode;\n    }>;\n    containerDivRef: RefObject<HTMLDivElement | null>;\n    cacheNodeClassName: string;\n    renderCount: number;\n    active: boolean;\n    cacheKey: string;\n    transition: boolean;\n    viewTransition: boolean;\n    duration: number;\n    exclude?: Array<string | RegExp> | string | RegExp;\n    include?: Array<string | RegExp> | string | RegExp;\n    destroy: (cacheKey: string | string[]) => Promise<void>;\n    enableActivity: boolean;\n}\n\nconst cacheDivMarkedClassName = \"keepalive-cache-div\";\n\nfunction getChildNodes(dom?: HTMLDivElement) {\n    return dom ? Array.from(dom.children) : [];\n}\n\nfunction removeDivNodes(nodes: Element[]) {\n    nodes.forEach(node => {\n        if (node.classList.contains(cacheDivMarkedClassName)) {\n            node.remove();\n        }\n    });\n}\n\nfunction renderCacheDiv(containerDiv: HTMLDivElement, cacheDiv: HTMLDivElement) {\n    const removeNodes = getChildNodes(containerDiv);\n    removeDivNodes(removeNodes);\n    containerDiv.appendChild(cacheDiv);\n    cacheDiv.classList.remove(\"inactive\");\n    cacheDiv.classList.add(\"active\");\n}\n\nfunction switchActiveNodesToInactive(containerDiv: HTMLDivElement, cacheKey: string) {\n    const nodes = getChildNodes(containerDiv);\n    const activeNodes = nodes.filter(node => node.classList.contains(\"active\") && node.getAttribute(\"data-cache-key\") !== cacheKey);\n    activeNodes.forEach(node => {\n        node.classList.remove(\"active\");\n        node.classList.add(\"inactive\");\n    });\n    return activeNodes;\n}\n\nfunction isCached(\n    cacheKey: string,\n    exclude?: Array<string | RegExp> | string | RegExp,\n    include?: Array<string | RegExp> | string | RegExp,\n) {\n    if (include) {\n        return isInclude(include, cacheKey);\n    } else {\n        if (exclude) {\n            return !isInclude(exclude, cacheKey);\n        }\n        return true;\n    }\n}\n\nconst CacheComponent = memo(\n    function (props: CacheComponentProps): any {\n        const { errorElement: ErrorBoundary = Fragment, cacheNodeClassName, children, cacheKey, exclude, include, enableActivity } = props;\n        const { active, renderCount, destroy, transition, viewTransition, duration, containerDivRef } = props;\n        const activatedRef = useRef(false);\n\n        activatedRef.current = activatedRef.current || active;\n\n        const cacheDiv = useMemo(() => {\n            const cacheDiv = document.createElement(\"div\");\n            domAttrSet(cacheDiv)\n                .set(\"data-cache-key\", cacheKey)\n                .set(\"style\", \"height: 100%\")\n                .set(\"data-render-count\", renderCount.toString());\n            cacheDiv.className = cacheDivMarkedClassName + (cacheNodeClassName ? ` ${cacheNodeClassName}` : \"\");\n            return cacheDiv;\n        }, [renderCount, cacheNodeClassName]);\n\n        useLayoutEffect(() => {\n            const cached = isCached(cacheKey, exclude, include);\n            const containerDiv = containerDivRef.current;\n            if (!containerDiv) {\n                console.warn(`keepalive: cache container not found`);\n                return;\n            }\n            if (transition) {\n                (async () => {\n                    if (active) {\n                        const inactiveNodes = switchActiveNodesToInactive(containerDiv, cacheKey);\n                        // duration - 40ms is to avoid the animation effect ending too early\n                        await delayAsync(duration - 40);\n                        removeDivNodes(inactiveNodes);\n                        if (containerDiv.contains(cacheDiv)) {\n                            return;\n                        }\n                        renderCacheDiv(containerDiv, cacheDiv);\n                    } else {\n                        if (!cached) {\n                            await delayAsync(duration);\n                            destroy(cacheKey);\n                        }\n                    }\n                })();\n            } else {\n                if (active) {\n                    const makeChange = () => {\n                        const inactiveNodes = switchActiveNodesToInactive(containerDiv, cacheKey);\n                        removeDivNodes(inactiveNodes);\n                        if (containerDiv.contains(cacheDiv)) {\n                            return;\n                        }\n                        renderCacheDiv(containerDiv, cacheDiv);\n                    };\n                    if (viewTransition && (document as any).startViewTransition) {\n                        (document as any).startViewTransition(makeChange);\n                    } else {\n                        makeChange();\n                    }\n                } else {\n                    if (!cached) {\n                        destroy(cacheKey);\n                    }\n                }\n            }\n        }, [active, containerDivRef, cacheKey, exclude, include]);\n\n        return activatedRef.current\n            ? createPortal(\n                  <ErrorBoundary>\n                      {hasNativeActivity && enableActivity ? (\n                          <MemoizedActivty active={active} duration={duration}>\n                              {children}\n                          </MemoizedActivty>\n                      ) : (\n                          children\n                      )}\n                  </ErrorBoundary>,\n                  cacheDiv,\n                  cacheKey,\n              )\n            : null;\n    },\n    (prevProps, nextProps) => {\n        return (\n            prevProps.active === nextProps.active &&\n            prevProps.renderCount === nextProps.renderCount &&\n            prevProps.children === nextProps.children &&\n            prevProps.exclude === nextProps.exclude &&\n            prevProps.include === nextProps.include\n        );\n    },\n);\n\nexport default CacheComponent;\n"
  },
  {
    "path": "packages/core/src/components/CacheComponentProvider/index.tsx",
    "content": "import { memo, ReactNode, useMemo } from \"react\";\nimport { CacheComponentContext, KeepAliveContext } from \"../CacheContext\";\n\ninterface CacheComponentProviderProps extends KeepAliveContext {\n    children?: ReactNode;\n}\n\nconst CacheComponentProvider = memo(function (props: CacheComponentProviderProps) {\n    const { children, active, refresh, destroy, destroyAll, destroyOther, getCacheNodes, _cacheKey } = props;\n    const value = useMemo(() => {\n        return { active, refresh, destroy, destroyAll, destroyOther, getCacheNodes, _cacheKey };\n    }, [active, refresh, destroy, destroyAll, destroyOther, getCacheNodes, _cacheKey]);\n    return <CacheComponentContext.Provider value={value}>{children}</CacheComponentContext.Provider>;\n});\n\nexport default CacheComponentProvider;\n"
  },
  {
    "path": "packages/core/src/components/CacheContext/index.tsx",
    "content": "import { createContext } from \"react\";\nimport { KeepAliveAPI } from \"../KeepAlive\";\n\nexport interface KeepAliveContext extends KeepAliveAPI {\n    /**\n     * whether the component is active\n     */\n    active: boolean;\n    /**\n     * the cache key of the component\n     */\n    _cacheKey: string;\n}\n\nexport const CacheComponentContext = createContext<KeepAliveContext>({\n    active: false,\n    _cacheKey: \"\",\n    refresh: () => {},\n    destroy: () => Promise.resolve(),\n    destroyAll: () => Promise.resolve(),\n    destroyOther: () => Promise.resolve(),\n    getCacheNodes: () => [],\n});\n"
  },
  {
    "path": "packages/core/src/components/KeepAlive/index.tsx",
    "content": "import {\n    ComponentType,\n    Fragment,\n    ReactElement,\n    ReactNode,\n    RefObject,\n    useCallback,\n    useImperativeHandle,\n    useLayoutEffect,\n    useRef,\n    useState,\n} from \"react\";\nimport { isArr, isFn, isNil, isRegExp, macroTask } from \"../../utils\";\nimport CacheComponentProvider from \"../CacheComponentProvider\";\nimport CacheComponent from \"../CacheComponent\";\nimport safeStartTransition from \"../../compat/safeStartTransition\";\nimport eventBus from \"../../event\";\n\nexport type KeepAliveChildren = ReactNode | ReactElement | null | undefined;\n\nexport interface KeepAliveProps {\n    activeCacheKey: string;\n    children?: KeepAliveChildren;\n    /**\n     * max cache count default 10\n     */\n    max?: number;\n    exclude?: Array<string | RegExp> | string | RegExp;\n    include?: Array<string | RegExp> | string | RegExp;\n    onBeforeActive?: (activeCacheKey: string) => void;\n    customContainerRef?: RefObject<HTMLDivElement> | undefined;\n    cacheNodeClassName?: string;\n    containerClassName?: string;\n    errorElement?: ComponentType<{\n        children: ReactNode;\n    }>;\n    /**\n     * transition default false\n     */\n    transition?: boolean;\n\n    /**\n     * view transition default false\n     *\n     * use viewTransition to animate the component when switching tabs\n     *\n     * @see https://developer.chrome.com/docs/web-platform/view-transitions/\n     */\n    viewTransition?: boolean;\n    /**\n     * transition duration default 200\n     */\n    duration?: number;\n    aliveRef?: RefObject<KeepAliveRef | undefined | null>;\n    /**\n     * max alive time for cache node (second)\n     * @default 0 (no limit)\n     */\n    maxAliveTime?: number | MaxAliveConfig[];\n\n    /**\n     * enable Activity component from react 19+\n     * @default false\n     * Activity component can improve performance, but it will affect the transition effect\n     * Attention: if enable Activity component, useEffect will trigger when the component is active\n     */\n    enableActivity?: boolean;\n}\n\ninterface MaxAliveConfig {\n    match: string | RegExp;\n    expire: number;\n}\n\nexport interface CacheNode {\n    cacheKey: string;\n    ele?: KeepAliveChildren;\n    lastActiveTime: number;\n    renderCount: number;\n}\n\nexport interface KeepAliveAPI {\n    /**\n     * Refreshes the component.\n     * @param {string} [cacheKey] - The cache key of the component. If not provided, the current cached component will be refreshed.\n     */\n    refresh: (cacheKey?: string) => void;\n    /**\n     * destroy the component\n     * @param {string} [cacheKey] - the cache key of the component, if not provided, current active cached component will be destroyed\n     */\n    destroy: (cacheKey?: string | string[]) => Promise<void>;\n    /**\n     * destroy all components\n     */\n    destroyAll: () => Promise<void>;\n    /**\n     * destroy other components except the provided cacheKey\n     * @param {string} [cacheKey] - The cache key of the component. If not provided, destroy all components except the current active cached component.\n     */\n    destroyOther: (cacheKey?: string) => Promise<void>;\n    /**\n     * get the cache nodes\n     */\n    getCacheNodes: () => Array<CacheNode>;\n}\n\nexport interface KeepAliveRef extends KeepAliveAPI {}\n\nexport function useKeepAliveRef() {\n    return useRef<KeepAliveRef>(null);\n}\n\nfunction KeepAlive(props: KeepAliveProps) {\n    const {\n        activeCacheKey,\n        max = 10,\n        exclude,\n        include,\n        onBeforeActive,\n        customContainerRef,\n        cacheNodeClassName = `cache-component`,\n        containerClassName = \"keep-alive-render\",\n        errorElement,\n        transition = false,\n        viewTransition = false,\n        duration = 200,\n        children,\n        aliveRef,\n        maxAliveTime = 0,\n        enableActivity = false,\n    } = props;\n\n    const containerDivRef = customContainerRef || useRef<HTMLDivElement>(null);\n    const [cacheNodes, setCacheNodes] = useState<Array<CacheNode>>([]);\n\n    useLayoutEffect(() => {\n        if (isNil(activeCacheKey)) return;\n        safeStartTransition(() => {\n            setCacheNodes(prevCacheNodes => {\n                const lastActiveTime = Date.now();\n                const cacheNode = prevCacheNodes.find(item => item.cacheKey === activeCacheKey);\n                if (cacheNode) {\n                    return prevCacheNodes.map(item => {\n                        if (item.cacheKey === activeCacheKey) {\n                            let needUpdate = false;\n                            if (isFn(onBeforeActive)) onBeforeActive(activeCacheKey);\n                            if (maxAliveTime) {\n                                const prev = item.lastActiveTime;\n                                if (isArr(maxAliveTime)) {\n                                    const config = maxAliveTime.find(item => {\n                                        return isRegExp(item.match) ? item.match.test(activeCacheKey) : item.match === activeCacheKey;\n                                    });\n                                    if (config) {\n                                        needUpdate = config && prev + config.expire * 1000 < lastActiveTime;\n                                    }\n                                } else {\n                                    needUpdate = prev + maxAliveTime * 1000 < lastActiveTime;\n                                }\n                            }\n                            if (needUpdate) {\n                                eventBus.emit(\"destroy\", [activeCacheKey]);\n                            }\n                            return {\n                                ...item,\n                                ele: children,\n                                lastActiveTime,\n                                renderCount: needUpdate ? item.renderCount + 1 : item.renderCount,\n                            };\n                        }\n                        return item;\n                    });\n                } else {\n                    if (isFn(onBeforeActive)) onBeforeActive(activeCacheKey);\n                    if (prevCacheNodes.length > max) {\n                        const node = prevCacheNodes.reduce((prev, cur) => {\n                            return prev.lastActiveTime < cur.lastActiveTime ? prev : cur;\n                        });\n                        const deletedNodes = prevCacheNodes.splice(prevCacheNodes.indexOf(node), 1);\n                        const deletedCacheKeys = deletedNodes.map(item => item.cacheKey);\n                        eventBus.emit(\"destroy\", deletedCacheKeys);\n                    }\n                    return [...prevCacheNodes, { cacheKey: activeCacheKey, lastActiveTime, ele: children, renderCount: 0 }];\n                }\n            });\n        });\n    }, [activeCacheKey, children]);\n\n    const refresh = useCallback(\n        (cacheKey?: string) => {\n            setCacheNodes(cacheNodes => {\n                const targetCacheKey = cacheKey || activeCacheKey;\n                eventBus.emit(\"refresh\", targetCacheKey);\n                return cacheNodes.map(item => {\n                    if (item.cacheKey === targetCacheKey) {\n                        return { ...item, renderCount: item.renderCount + 1 };\n                    }\n                    return item;\n                });\n            });\n        },\n        [setCacheNodes, activeCacheKey],\n    );\n\n    const destroy = useCallback(\n        (cacheKey?: string | string[]) => {\n            const targetCacheKey = cacheKey || activeCacheKey;\n            const cacheKeys = isArr(targetCacheKey) ? targetCacheKey : [targetCacheKey];\n            eventBus.emit(\"destroy\", cacheKeys);\n            return new Promise<void>(resolve => {\n                macroTask(() => {\n                    setCacheNodes(cacheNodes => {\n                        return [...cacheNodes.filter(item => !cacheKeys.includes(item.cacheKey))];\n                    });\n                    resolve();\n                });\n            });\n        },\n        [setCacheNodes, activeCacheKey],\n    );\n\n    const destroyAll = useCallback(() => {\n        return new Promise<void>(resolve => {\n            eventBus.emit(\"destroyAll\");\n            macroTask(() => {\n                setCacheNodes([]);\n                resolve();\n            });\n        });\n    }, [setCacheNodes]);\n\n    const destroyOther = useCallback(\n        (cacheKey?: string) => {\n            const targetCacheKey = cacheKey || activeCacheKey;\n            return new Promise<void>(resolve => {\n                eventBus.emit(\"destroyOther\", targetCacheKey);\n                macroTask(() => {\n                    setCacheNodes(cacheNodes => {\n                        return [...cacheNodes.filter(item => item.cacheKey === targetCacheKey)];\n                    });\n                    resolve();\n                });\n            });\n        },\n        [activeCacheKey, setCacheNodes],\n    );\n\n    const getCacheNodes = useCallback(() => {\n        return cacheNodes;\n    }, [cacheNodes]);\n\n    useImperativeHandle(aliveRef, () => ({\n        refresh,\n        destroy,\n        destroyAll,\n        destroyOther,\n        getCacheNodes,\n    }));\n\n    return (\n        <Fragment>\n            <div ref={containerDivRef} className={containerClassName} style={{ height: \"100%\" }}></div>\n            {cacheNodes.map(item => {\n                const { cacheKey, ele, renderCount } = item;\n                return (\n                    <CacheComponentProvider\n                        key={`${cacheKey}-${renderCount}`}\n                        active={activeCacheKey === cacheKey}\n                        refresh={refresh}\n                        destroy={destroy}\n                        destroyAll={destroyAll}\n                        destroyOther={destroyOther}\n                        getCacheNodes={getCacheNodes}\n                        _cacheKey={cacheKey}\n                    >\n                        <CacheComponent\n                            destroy={destroy}\n                            include={include}\n                            exclude={exclude}\n                            transition={transition}\n                            viewTransition={viewTransition}\n                            duration={duration}\n                            renderCount={renderCount}\n                            containerDivRef={containerDivRef}\n                            errorElement={errorElement}\n                            active={activeCacheKey === cacheKey}\n                            cacheNodeClassName={cacheNodeClassName}\n                            cacheKey={cacheKey}\n                            enableActivity={enableActivity}\n                        >\n                            {ele}\n                        </CacheComponent>\n                    </CacheComponentProvider>\n                );\n            })}\n        </Fragment>\n    );\n}\n\nexport default KeepAlive;\n"
  },
  {
    "path": "packages/core/src/components/MemoizedActivty/index.tsx",
    "content": "import { Activity } from \"../../compat/Activity\";\nimport { ReactNode, useLayoutEffect, useRef, useState, memo, startTransition } from \"react\";\n\ninterface MemoizedActivtyProps {\n    children: ReactNode;\n    active: boolean;\n    duration: number;\n}\n\nfunction _MemoizedActivty({ children, active, duration }: MemoizedActivtyProps) {\n    const [delayedActive, setDelayedActive] = useState(active);\n    const delayedActiveTimerRef = useRef<null | ReturnType<typeof setTimeout>>(null);\n    useLayoutEffect(() => {\n        if (active) {\n            startTransition(() => {\n                setDelayedActive(true);\n            });\n        } else {\n            if (delayedActiveTimerRef.current) {\n                clearTimeout(delayedActiveTimerRef.current);\n            }\n            delayedActiveTimerRef.current = setTimeout(() => {\n                setDelayedActive(false);\n                if (delayedActiveTimerRef.current) {\n                    clearTimeout(delayedActiveTimerRef.current);\n                }\n            }, duration);\n        }\n        return () => {\n            if (delayedActiveTimerRef.current) {\n                clearTimeout(delayedActiveTimerRef.current);\n            }\n        };\n    }, [active]);\n    return <Activity mode={delayedActive ? \"visible\" : \"hidden\"}>{children}</Activity>;\n}\n\nconst MemoizedActivty = memo(_MemoizedActivty, (prevProps, nextProps) => {\n    return prevProps.active === nextProps.active && prevProps.duration === nextProps.duration;\n});\n\nMemoizedActivty.displayName = \"MemoizedActivty\";\n\nexport default MemoizedActivty;\n"
  },
  {
    "path": "packages/core/src/event/index.ts",
    "content": "import mitt, { Emitter } from \"mitt\";\n\ntype Events = {\n    destroy: string[];\n    refresh: string;\n    destroyAll: void;\n    destroyOther: string;\n};\n\nconst eventBus: Emitter<Events> = mitt<Events>();\n\nexport default eventBus;\n"
  },
  {
    "path": "packages/core/src/hooks/onDestory.ts",
    "content": "import eventBus from \"../event\";\n\nfunction useOnDestroy(cb: () => any, _key: string) {\n    eventBus.on(\"destroy\", cacheKeys => {\n        if (cacheKeys.includes(_key)) {\n            cb();\n        }\n    });\n    eventBus.on(\"destroyAll\", () => {\n        cb();\n    });\n    eventBus.on(\"destroyOther\", cacheKey => {\n        if (cacheKey !== _key) {\n            cb();\n        }\n    });\n    eventBus.on(\"refresh\", cacheKey => {\n        if (cacheKey === _key) {\n            cb();\n        }\n    });\n}\n\nexport default useOnDestroy;\n"
  },
  {
    "path": "packages/core/src/hooks/useEffectOnActive.ts",
    "content": "import { DependencyList, useEffect } from \"react\";\nimport useOnActive from \"./useOnActive\";\n\nconst useEffectOnActive = (cb: () => any, deps: DependencyList, skipMount = false): void => {\n    useOnActive(cb, deps, skipMount, useEffect);\n};\n\nexport default useEffectOnActive;\n"
  },
  {
    "path": "packages/core/src/hooks/useEffectOnCreate.ts",
    "content": "import { useEffect } from \"react\";\nimport useOnCreate from \"./useOnCreate\";\n\nconst useEffectOnCreate = (cb: () => any): void => {\n    useOnCreate(cb, useEffect);\n};\n\nexport default useEffectOnCreate;\n"
  },
  {
    "path": "packages/core/src/hooks/useKeepAliveContext.ts",
    "content": "import { useContext } from \"react\";\nimport { CacheComponentContext } from \"../components/CacheContext\";\n\nconst useKeepAliveContext = () => {\n    return useContext(CacheComponentContext);\n};\n\nexport default useKeepAliveContext;\n"
  },
  {
    "path": "packages/core/src/hooks/useLayoutEffectOnActive.ts",
    "content": "import { DependencyList, useLayoutEffect } from \"react\";\nimport useOnActive from \"./useOnActive\";\n\nconst useLayoutEffectOnActive = (cb: () => any, deps: DependencyList, skipMount = false): void => {\n    useOnActive(cb, deps, skipMount, useLayoutEffect);\n};\n\nexport default useLayoutEffectOnActive;\n"
  },
  {
    "path": "packages/core/src/hooks/useLayoutEffectOnCreate.ts",
    "content": "import { useLayoutEffect } from \"react\";\nimport useOnCreate from \"./useOnCreate\";\n\nconst useLayoutEffectOnCreate = (cb: () => any): void => {\n    useOnCreate(cb, useLayoutEffect);\n};\n\nexport default useLayoutEffectOnCreate;\n"
  },
  {
    "path": "packages/core/src/hooks/useOnActive.ts",
    "content": "import { DependencyList, useEffect, useLayoutEffect, useRef } from \"react\";\nimport useKeepAliveContext from \"./useKeepAliveContext\";\nimport { isFn } from \"../utils\";\n\nfunction useOnActive(cb: () => any, deps: DependencyList, skipMount = false, effect: typeof useEffect | typeof useLayoutEffect) {\n    const { active } = useKeepAliveContext();\n    const isMount = useRef<boolean>(false);\n    effect(() => {\n        if (!active) return;\n        if (skipMount && !isMount.current) {\n            isMount.current = true;\n            return;\n        }\n        const destroyCb = cb();\n        return () => {\n            if (isFn(destroyCb)) {\n                destroyCb();\n            }\n        };\n    }, [active, ...deps]);\n}\n\nexport default useOnActive;\n"
  },
  {
    "path": "packages/core/src/hooks/useOnCreate.ts",
    "content": "import { useEffect, useLayoutEffect, useRef } from \"react\";\nimport { isFn } from \"../utils\";\nimport useKeepAliveContext from \"./useKeepAliveContext\";\nimport useOnDestroy from \"./onDestory\";\n\nfunction useOnCreate(cb: () => any, effect: typeof useEffect | typeof useLayoutEffect) {\n    const isMount = useRef<boolean>(false);\n    const destroyedRef = useRef<boolean>(false);\n    const { _cacheKey } = useKeepAliveContext();\n    effect(() => {\n        let destroyCb: any;\n        if (isMount.current === false) {\n            isMount.current = true;\n            destroyCb = cb();\n        }\n        useOnDestroy(() => {\n            if (isFn(destroyCb) && !destroyedRef.current) {\n                destroyedRef.current = true;\n                destroyCb();\n            }\n        }, _cacheKey);\n    }, []);\n}\n\nexport default useOnCreate;\n"
  },
  {
    "path": "packages/core/src/index.ts",
    "content": "import KeepAlive, { KeepAliveProps, KeepAliveRef, useKeepAliveRef } from \"./components/KeepAlive\";\nimport useEffectOnActive from \"./hooks/useEffectOnActive\";\nimport useKeepAliveContext from \"./hooks/useKeepAliveContext\";\nimport useLayoutEffectOnActive from \"./hooks/useLayoutEffectOnActive\";\nimport useEffectOnCreate from \"./hooks/useEffectOnCreate\";\nimport useLayoutEffectOnCreate from \"./hooks/useLayoutEffectOnCreate\";\n\n/**\n * @deprecated since version 3.0.2. Use `useKeepAliveRef` instead.\n */\nconst useKeepaliveRef = useKeepAliveRef;\n\nexport {\n    KeepAlive,\n    useKeepAliveRef,\n    useKeepaliveRef,\n    useEffectOnActive,\n    useLayoutEffectOnActive,\n    useKeepAliveContext,\n    useEffectOnCreate,\n    useLayoutEffectOnCreate,\n};\n\nexport type { KeepAliveRef, KeepAliveProps };\n"
  },
  {
    "path": "packages/core/src/utils/index.tsx",
    "content": "export function isNil(value: any): value is null | undefined {\n    return value === null || value === undefined;\n}\n\nexport function isRegExp(value: any): value is RegExp {\n    return Object.prototype.toString.call(value) === \"[object RegExp]\";\n}\n\nexport function isArr(value: any): value is Array<any> {\n    return Array.isArray(value);\n}\n\nexport function isFn(value: any): value is Function {\n    return typeof value === \"function\";\n}\n\nexport function domAttrSet(dom: HTMLDivElement) {\n    return {\n        set: (key: string, value: string) => {\n            dom.setAttribute(key, value);\n            return domAttrSet(dom);\n        },\n    };\n}\n\nexport function delayAsync(milliseconds: number = 100): Promise<void> {\n    let _timeID: null | number | NodeJS.Timeout;\n    return new Promise<void>((resolve, _reject) => {\n        _timeID = setTimeout(() => {\n            resolve();\n            if (!isNil(_timeID)) {\n                clearTimeout(_timeID);\n            }\n        }, milliseconds);\n    });\n}\n\nexport function isInclude(include: Array<string | RegExp> | string | RegExp | undefined, val: string) {\n    const includes = isArr(include) ? include : isNil(include) ? [] : [include];\n    return includes.some(include => {\n        if (isRegExp(include)) {\n            return include.test(val);\n        } else {\n            return val === include;\n        }\n    });\n}\n\nexport function macroTask(fn: () => void) {\n    setTimeout(fn, 0);\n}\n"
  },
  {
    "path": "packages/core/tsconfig.json",
    "content": "{\n    \"extends\": \"../../tsconfig.json\",\n    \"compilerOptions\": {\n        \"rootDir\": \"src\",\n        \"outDir\": \"dist\"\n    },\n    \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/core/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n    entry: [\"src/index.ts\"],\n    format: [\"cjs\", \"esm\"],\n    outExtension({ format }) {\n        return {\n            js: format === \"cjs\" ? \".cjs\" : \".mjs\",\n        };\n    },\n    dts: true,\n    clean: true,\n    external: [\"react\", \"react-dom\", \"react/jsx-runtime\"],\n    minify: true,\n    treeshake: true,\n});\n"
  },
  {
    "path": "packages/router/CHANGELOG.md",
    "content": "# keepalive-for-react-router\n\n## 5.0.7\n\n### Patch Changes\n\n-   change enableActivity to default false\n-   Updated dependencies\n    -   keepalive-for-react@5.0.7\n\n## 5.0.5\n\n### Patch Changes\n\n-   fix import error\n\n## 5.0.4\n\n### Patch Changes\n\n-   bug fix\n-   bux fix\n-   bug fixes\n-   bug fixes\n-   Bug fixes\n-   bug fixes\n-   bug fixes\n-   bug fixes\n-   Updated dependencies\n-   Updated dependencies\n-   Updated dependencies\n-   Updated dependencies\n-   Updated dependencies\n-   Updated dependencies\n-   Updated dependencies\n-   Updated dependencies\n-   Updated dependencies\n    -   keepalive-for-react@5.0.4\n\n## 5.0.4-beta.8\n\n### Patch Changes\n\n-   bug fixes\n-   Updated dependencies\n    -   keepalive-for-react@5.0.4-beta.8\n\n## 5.0.4-beta.7\n\n### Patch Changes\n\n-   bug fixes\n-   Updated dependencies\n    -   keepalive-for-react@5.0.4-beta.7\n\n## 5.0.4-beta.6\n\n### Patch Changes\n\n-   bug fixes\n-   Updated dependencies\n    -   keepalive-for-react@5.0.4-beta.6\n\n## 5.0.4-beta.5\n\n### Patch Changes\n\n-   bug fixes\n-   Updated dependencies\n    -   keepalive-for-react@5.0.4-beta.5\n\n## 5.0.4-beta.4\n\n### Patch Changes\n\n-   bug fixes\n-   Updated dependencies\n    -   keepalive-for-react@5.0.4-beta.4\n\n## 5.0.4-beta.2\n\n### Patch Changes\n\n-   bug fixes\n\n## 5.0.4-beta.1\n\n### Patch Changes\n\n-   bug fixes\n-   Updated dependencies\n    -   keepalive-for-react@5.0.4-beta.1\n\n## 5.0.4-beta.0\n\n### Patch Changes\n\n-   Bug fixes\n-   Updated dependencies\n    -   keepalive-for-react@5.0.4-beta.0\n"
  },
  {
    "path": "packages/router/README.md",
    "content": "# KeepAlive for React Router\n\n## Installation\n\n```bash\nnpm install keepalive-for-react keepalive-for-react-router\n```\n\n### v6+\n\n```bash\nnpm install react-router-dom keepalive-for-react keepalive-for-react-router@1.x.x\n```\n\n### v7+\n\n```bash\nnpm install react-router keepalive-for-react keepalive-for-react-router@2.x.x\n```\n\n## Usage\n\n```tsx\n// v6+ keepalive-for-react-router@1.x.x\n// v7+ keepalive-for-react-router@2.x.x\nimport KeepAliveRouteOutlet from \"keepalive-for-react-router\";\n\nfunction Layout() {\n    return (\n        <div className=\"layout\">\n            <KeepAliveRouteOutlet />\n        </div>\n    );\n}\n```\n"
  },
  {
    "path": "packages/router/package.json",
    "content": "{\n    \"name\": \"keepalive-for-react-router\",\n    \"version\": \"5.0.7\",\n    \"description\": \"React Router integration for keepalive-for-react\",\n    \"homepage\": \"https://github.com/finedaybreak/keepalive-for-react\",\n    \"repository\": {\n        \"type\": \"git\",\n        \"url\": \"git+https://github.com/finedaybreak/keepalive-for-react.git\"\n    },\n    \"exports\": {\n        \".\": {\n            \"types\": \"./dist/index.d.ts\",\n            \"import\": \"./dist/index.mjs\",\n            \"require\": \"./dist/index.cjs\"\n        }\n    },\n    \"main\": \"./dist/index.cjs\",\n    \"module\": \"./dist/index.mjs\",\n    \"types\": \"./dist/index.d.ts\",\n    \"files\": [\n        \"dist\"\n    ],\n    \"type\": \"module\",\n    \"scripts\": {\n        \"build\": \"tsup\",\n        \"clean\": \"rm -rf dist\"\n    },\n    \"keywords\": [\n        \"keepalive\",\n        \"keep-alive\",\n        \"react keepalive\",\n        \"keepalive for react\",\n        \"keepalive-for-react\",\n        \"keepalive-for-react-router\"\n    ],\n    \"author\": \"wongyichen\",\n    \"license\": \"MIT\",\n    \"peerDependencies\": {\n        \"keepalive-for-react\": \"^5.0.7\",\n        \"react\": \">=16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0\",\n        \"react-dom\": \">=16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0\",\n        \"react-router\": \">=6.0.0\"\n    },\n    \"devDependencies\": {\n        \"keepalive-for-react\": \"^5.0.7\",\n        \"react-router\": \"^7.10.0\"\n    }\n}\n"
  },
  {
    "path": "packages/router/src/components/KeepAliveRouteOutlet/index.tsx",
    "content": "import { ComponentType, Fragment, ReactNode, useMemo } from \"react\";\nimport { useLocation, useOutlet } from \"react-router\";\nimport { KeepAlive, type KeepAliveProps } from \"keepalive-for-react\";\n\nexport interface KeepAliveRouteOutletProps extends Omit<KeepAliveProps, \"activeCacheKey\"> {\n    wrapperComponent?: ComponentType<{ children: ReactNode }>;\n    activeCacheKey?: string;\n}\n\nfunction KeepAliveRouteOutlet(props: KeepAliveRouteOutletProps) {\n    const { wrapperComponent, activeCacheKey: propsActiveCacheKey, ...rest } = props;\n    const location = useLocation();\n    const outlet = useOutlet();\n    const WrapperComponent = wrapperComponent || Fragment;\n\n    const activeCacheKey = useMemo(() => {\n        return propsActiveCacheKey || location.pathname + location.search;\n    }, [location.pathname, location.search, propsActiveCacheKey]);\n\n    return (\n        <KeepAlive {...rest} activeCacheKey={activeCacheKey}>\n            <WrapperComponent>{outlet}</WrapperComponent>\n        </KeepAlive>\n    );\n}\n\nexport default KeepAliveRouteOutlet;\n"
  },
  {
    "path": "packages/router/src/index.ts",
    "content": "import KeepAliveRouteOutlet from \"./components/KeepAliveRouteOutlet\";\n\nexport default KeepAliveRouteOutlet;\n"
  },
  {
    "path": "packages/router/tsconfig.json",
    "content": "{\n    \"extends\": \"../../tsconfig.json\",\n    \"compilerOptions\": {\n        \"rootDir\": \"src\",\n        \"outDir\": \"dist\"\n    },\n    \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/router/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n    entry: [\"src/index.ts\"],\n    format: [\"cjs\", \"esm\"],\n    outExtension({ format }) {\n        return {\n            js: format === \"cjs\" ? \".cjs\" : \".mjs\",\n        };\n    },\n    dts: true,\n    clean: true,\n    external: [\"react\", \"react-dom\", \"react-router\", \"react/jsx-runtime\", \"keepalive-for-react\"],\n    minify: true,\n    treeshake: true,\n});\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n    - \"packages/*\"\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n    \"compilerOptions\": {\n        \"target\": \"ES2020\",\n        \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n        \"module\": \"ESNext\",\n        \"moduleResolution\": \"bundler\",\n        \"jsx\": \"react-jsx\",\n        \"strict\": true,\n        \"skipLibCheck\": true,\n        \"isolatedModules\": true,\n        \"resolveJsonModule\": true,\n        \"esModuleInterop\": true,\n        \"allowSyntheticDefaultImports\": true,\n        \"noUnusedLocals\": true,\n        \"noUnusedParameters\": true,\n        \"noFallthroughCasesInSwitch\": true,\n        \"declaration\": true,\n        \"declarationMap\": true\n    },\n    \"exclude\": [\"node_modules\", \"**/dist\"]\n}\n"
  }
]