Repository: irychen/keepalive-for-react Branch: main Commit: 39671211ed31 Files: 94 Total size: 121.0 KB Directory structure: gitextract_xi2rhxav/ ├── .changeset/ │ ├── README.md │ └── config.json ├── .claude/ │ └── settings.json ├── .github/ │ └── ISSUE_TEMPLATE/ │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md ├── .gitignore ├── .husky/ │ └── pre-commit ├── .prettierrc ├── CLAUDE.md ├── LICENSE ├── README.md ├── README.zh_CN.md ├── examples/ │ ├── react-router-dom-simple-starter/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── README.md │ │ ├── eslint.config.js │ │ ├── index.html │ │ ├── package.json │ │ ├── postcss.config.js │ │ ├── src/ │ │ │ ├── App.tsx │ │ │ ├── index.css │ │ │ ├── layout/ │ │ │ │ └── index.tsx │ │ │ ├── main.tsx │ │ │ ├── pages/ │ │ │ │ ├── about/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── counter/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── home/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── nested/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── nested-a/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── nested-b/ │ │ │ │ │ └── index.tsx │ │ │ │ └── nocache-counter/ │ │ │ │ └── index.tsx │ │ │ ├── router/ │ │ │ │ └── index.tsx │ │ │ └── vite-env.d.ts │ │ ├── tailwind.config.js │ │ ├── tsconfig.app.json │ │ ├── tsconfig.app.tsbuildinfo │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ ├── tsconfig.node.tsbuildinfo │ │ └── vite.config.ts │ └── simple-tabs-starter/ │ ├── .gitignore │ ├── .npmrc │ ├── README.md │ ├── eslint.config.js │ ├── index.html │ ├── package.json │ ├── pnpm-workspace.yaml │ ├── postcss.config.js │ ├── src/ │ │ ├── App.tsx │ │ ├── index.css │ │ ├── main.tsx │ │ ├── store/ │ │ │ └── counter-store.tsx │ │ └── vite-env.d.ts │ ├── tailwind.config.js │ ├── tsconfig.app.json │ ├── tsconfig.app.tsbuildinfo │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── tsconfig.node.tsbuildinfo │ └── vite.config.ts ├── package.json ├── packages/ │ ├── core/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── README.zh_CN.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── compat/ │ │ │ │ ├── Activity.tsx │ │ │ │ └── safeStartTransition.ts │ │ │ ├── components/ │ │ │ │ ├── CacheComponent/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── CacheComponentProvider/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── CacheContext/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── KeepAlive/ │ │ │ │ │ └── index.tsx │ │ │ │ └── MemoizedActivty/ │ │ │ │ └── index.tsx │ │ │ ├── event/ │ │ │ │ └── index.ts │ │ │ ├── hooks/ │ │ │ │ ├── onDestory.ts │ │ │ │ ├── useEffectOnActive.ts │ │ │ │ ├── useEffectOnCreate.ts │ │ │ │ ├── useKeepAliveContext.ts │ │ │ │ ├── useLayoutEffectOnActive.ts │ │ │ │ ├── useLayoutEffectOnCreate.ts │ │ │ │ ├── useOnActive.ts │ │ │ │ └── useOnCreate.ts │ │ │ ├── index.ts │ │ │ └── utils/ │ │ │ └── index.tsx │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ └── router/ │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src/ │ │ ├── components/ │ │ │ └── KeepAliveRouteOutlet/ │ │ │ └── index.tsx │ │ └── index.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── pnpm-workspace.yaml └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .changeset/README.md ================================================ # Changesets Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets) We have a quick list of common questions to get you started engaging with this project in [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) ================================================ FILE: .changeset/config.json ================================================ { "$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json", "changelog": "@changesets/cli/changelog", "commit": false, "fixed": [], "linked": [["keepalive-for-react", "keepalive-for-react-router"]], "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", "ignore": [] } ================================================ FILE: .claude/settings.json ================================================ { "permissions": { "defaultMode": "bypassPermissions" }, "includeCoAuthoredBy": false, "attribution": { "commit": "", "pr": "" }, "env": { "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1" } } ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/custom.md ================================================ --- name: Custom issue template about: Describe this issue template's purpose here. title: '' labels: '' assignees: '' --- ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .gitignore ================================================ node_modules # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? dist ================================================ FILE: .husky/pre-commit ================================================ pnpm exec lint-staged ================================================ FILE: .prettierrc ================================================ { "printWidth": 140, "tabWidth": 4, "useTabs": false, "singleQuote": false, "jsxSingleQuote": false, "semi": true, "trailingComma": "all", "bracketSpacing": true, "arrowParens": "avoid" } ================================================ FILE: CLAUDE.md ================================================ # CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Repository layout pnpm workspace (see `pnpm-workspace.yaml`). Two publishable packages, plus two runnable examples: - `packages/core` → `keepalive-for-react` — the `` component and hooks. Only runtime dep is `mitt`. - `packages/router` → `keepalive-for-react-router` — thin `` wrapper that reads `react-router`'s `useLocation`/`useOutlet` and feeds them into core. Peer-depends on `keepalive-for-react` and `react-router >= 6`. - `examples/react-router-dom-simple-starter`, `examples/simple-tabs-starter` — Vite + React playgrounds used to smoke-test changes. Versions 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). ## Commands All run from repo root unless noted: ```bash pnpm install # install workspace pnpm build # tsup build for every package (pnpm -r build) pnpm clean # rm -rf dist in every package pnpm format # prettier --write on ts/tsx/json pnpm changeset # author a changeset pnpm version # apply changesets (bump versions + changelogs) pnpm release # build + changeset publish pnpm example:router # run the react-router example pnpm example:tabs # run the simple-tabs example ``` Per-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. Pre-commit: husky runs `lint-staged` which runs prettier on staged files. ## Architecture ### Core runtime model (`packages/core/src/components/KeepAlive/index.tsx`) `KeepAlive` holds a `cacheNodes: CacheNode[]` state (`{ cacheKey, ele, lastActiveTime, renderCount }`). On every `activeCacheKey`/`children` change (inside `useLayoutEffect` + `safeStartTransition`): - 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. - If not → push a new node. When length exceeds `max`, the LRU node (lowest `lastActiveTime`) is evicted and `destroy` is emitted for its key. `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. `aliveRef` exposes this API imperatively via `useImperativeHandle`; `useKeepAliveRef()` is just `useRef(null)`. ### DOM hand-off (`packages/core/src/components/CacheComponent/index.tsx`) Each cache node renders into its own imperatively-created `
` 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. Three swap modes, picked from props: - **`transition`**: mark prev siblings `.inactive`, wait `duration - 40ms`, remove them, then append the new div. Relies on user CSS keying off `.active`/`.inactive`. - **`viewTransition`**: wraps the sync swap in `document.startViewTransition(...)`. - **default**: synchronous swap inside the same `useLayoutEffect`. When 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. A cache node only renders its children once `activatedRef.current` has ever been true, so cached-but-never-visited children are not mounted. ### Context + hooks `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. - `useEffectOnActive` / `useLayoutEffectOnActive` — via `useOnActive`, skip when `!active`, optional `skipMount` to no-op the first render. - `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`. - `useKeepAliveContext` — direct read of the context. ### React version compatibility (`packages/core/src/compat/`) - `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. - `safeStartTransition.ts` falls back to a synchronous call when `startTransition` isn't defined (< React 18). - `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. Peer-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. ### Router adapter (`packages/router/src/components/KeepAliveRouteOutlet/index.tsx`) Tiny 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. ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2024 Rychen Wong Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

keepalive-for-react logo

KeepAlive for React

A React KeepAlive component like keep-alive in vue

[中文](./README.zh_CN.md) | English [![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]
## Packages | Package | Version | Description | | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | | [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 | | [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 | ## Features - Support react-router-dom v6+ or react-router v7+ - Support React v16+ ~ v18+ (v19.2 Activity component support [v5.0.0]) - Support Suspense and Lazy import - Support ErrorBoundary - Support Custom Container - Support Switching Animation Transition with className `active` and `inactive` - Simply implement, without any extra dependencies and hacking ways - Only 6KB minified size - Support interrupt state effect when component is not active (v5.0.0) ## Attention - **Version Compatibility**: - For React 18, please use `keepalive-for-react@4.x.x` - For React 19.2+, please use `keepalive-for-react@5.x.x` - DO NOT use , it CANNOT work with keepalive-for-react in development mode. because it can lead to some unexpected behavior. - In Router only support react-router-dom v6+ ## Install ```bash npm install keepalive-for-react ``` ```bash yarn add keepalive-for-react ``` ```bash pnpm add keepalive-for-react ``` ## Usage ### in react-router-dom v6+ or react-router v7+ 1. install react-router-dom v6+ or react-router v7+ ```bash # v6+ npm install react-router-dom keepalive-for-react keepalive-for-react-router@1.x.x # v7+ npm install react-router keepalive-for-react keepalive-for-react-router@2.x.x ``` 2. use KeepAlive in your project ```tsx // v6+ keepalive-for-react-router@1.x.x // v7+ keepalive-for-react-router@2.x.x import KeepAliveRouteOutlet from "keepalive-for-react-router"; function Layout() { return (
); } ``` or ```tsx import { useMemo } from "react"; // v6+ import { useLocation, useOutlet } from "react-router-dom"; // v7 // import { useLocation, useOutlet } from "react-router"; import { KeepAlive, useKeepAliveRef } from "keepalive-for-react"; function Layout() { const location = useLocation(); const aliveRef = useKeepAliveRef(); const outlet = useOutlet(); // determine which route component to is active const currentCacheKey = useMemo(() => { return location.pathname + location.search; }, [location.pathname, location.search]); return (
}> {outlet}
); } ``` details see [examples/react-router-dom-simple-starter](./examples/react-router-dom-simple-starter) [![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) ### in simple tabs ```bash npm install keepalive-for-react ``` ```tsx const tabs = [ { key: "tab1", label: "Tab 1", component: Tab1, }, { key: "tab2", label: "Tab 2", component: Tab2, }, { key: "tab3", label: "Tab 3", component: Tab3, }, ]; function App() { const [currentTab, setCurrentTab] = useState("tab1"); const tab = useMemo(() => { return tabs.find(tab => tab.key === currentTab); }, [currentTab]); return (
{/* ... */} {tab && }
); } ``` details see [examples/simple-tabs-starter](./examples/simple-tabs-starter) [![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) ## KeepAlive Props type definition ```tsx interface KeepAliveProps { // determine which component to is active activeCacheKey: string; children?: KeepAliveChildren; /** * max cache count default 10 */ max?: number; exclude?: Array | string | RegExp; include?: Array | string | RegExp; onBeforeActive?: (activeCacheKey: string) => void; customContainerRef?: RefObject; cacheNodeClassName?: string; containerClassName?: string; errorElement?: ComponentType<{ children: ReactNode; }>; /** * transition default false */ transition?: boolean; /** * use view transition to animate the component when switching tabs * @see https://developer.chrome.com/docs/web-platform/view-transitions/ */ viewTransition?: boolean; /** * transition duration default 200 */ duration?: number; aliveRef?: RefObject; /** * max alive time for cache node (second) * @default 0 (no limit) */ maxAliveTime?: number | MaxAliveConfig[]; /** * enable Activity component from react 19.2+ * @default false * Attention: if enable Activity component, useEffect will trigger when the component is active */ enableActivity?: boolean; } interface MaxAliveConfig { match: string | RegExp; expire: number; } ``` ## Hooks ### useEffectOnActive ```tsx useEffectOnActive(() => { console.log("active"); }, []); ``` ### useLayoutEffectOnActive ```tsx useLayoutEffectOnActive( () => { console.log("active"); }, [], false, ); // the third parameter is optional, default is false, // if true, which means the callback will be skipped when the useLayoutEffect is triggered in first render ``` ### useEffectOnCreate Run 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. ```tsx useEffectOnCreate(() => { console.log("component created"); return () => { console.log("component destroyed"); }; }); ``` ### useLayoutEffectOnCreate Same as `useEffectOnCreate` but uses `useLayoutEffect` internally. Useful when the create-time logic needs to run synchronously before the browser paints. ```tsx useLayoutEffectOnCreate(() => { console.log("component created (layout)"); return () => { console.log("component destroyed (layout)"); }; }); ``` ### useKeepAliveContext type definition ```ts interface KeepAliveContext { /** * whether the component is active */ active: boolean; /** * refresh the component * @param {string} [cacheKey] - The cache key of the component. If not provided, the current cached component will be refreshed. */ refresh: (cacheKey?: string) => void; /** * destroy the component * @param {string} [cacheKey] - the cache key of the component, if not provided, current active cached component will be destroyed */ destroy: (cacheKey?: string | string[]) => Promise; /** * destroy all components */ destroyAll: () => Promise; /** * destroy other components except the provided cacheKey * @param {string} [cacheKey] - The cache key of the component. If not provided, destroy all components except the current active cached component. */ destroyOther: (cacheKey?: string) => Promise; /** * get the cache nodes */ getCacheNodes: () => Array; } ``` ```tsx const { active, refresh, destroy, getCacheNodes } = useKeepAliveContext(); // active is a boolean, true is active, false is inactive // refresh is a function, you can call it to refresh the component // destroy is a function, you can call it to destroy the component // ... // getCacheNodes is a function, you can call it to get the cache nodes ``` ### useKeepAliveRef type definition ```ts interface KeepAliveRef { refresh: (cacheKey?: string) => void; destroy: (cacheKey?: string | string[]) => Promise; destroyAll: () => Promise; destroyOther: (cacheKey?: string) => Promise; getCacheNodes: () => Array; } ``` ```tsx function App() { const aliveRef = useKeepAliveRef(); // aliveRef.current is a KeepAliveRef object // you can call refresh and destroy on aliveRef.current aliveRef.current?.refresh(); // it is not necessary to call destroy manually, KeepAlive will handle it automatically aliveRef.current?.destroy(); return {/* ... */}; } // or function AppRouter() { const aliveRef = useKeepAliveRef(); // aliveRef.current is a KeepAliveRef object // you can call refresh and destroy on aliveRef.current aliveRef.current?.refresh(); aliveRef.current?.destroy(); return ; } ``` ## Development install dependencies ```bash pnpm install ``` build package ```bash pnpm build ``` [discord-link]: https://discord.gg/ycf896w7eA [discord-shield]: https://img.shields.io/discord/1232158668913381467?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square [discord-shield-badge]: https://img.shields.io/discord/1232158668913381467?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=for-the-badge ================================================ FILE: README.zh_CN.md ================================================

keepalive-for-react logo

React KeepAlive 组件

一个类似Vue中keep-alive的React KeepAlive组件

[English](./README.md) | 中文 [![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) ## 包信息 | 包名 | 版本 | 描述 | | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | [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功能 | | [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集成 | ## 特性 - 支持react-router-dom v6+ 或 react-router v7+ - 支持React v16+ ~ v18+ (v19.2 Activity component support [v5.0.0]) - 支持Suspense和懒加载导入 - 支持错误边界 - 支持自定义容器 - 支持使用className `active`和`inactive`进行切换动画过渡 - 简单实现,无需任何额外依赖和hack方式 - 压缩后仅6KB大小 - 支持中断state Effect当组件不活动时 (v5.0.0) ## 注意事项 - **版本兼容性**: - React 18 请使用 `keepalive-for-react@4.x.x` - React 19.2+ 请使用 `keepalive-for-react@5.x.x` - 请勿使用 ,它在开发模式下无法与keepalive-for-react一起工作。因为它可能会导致一些意外行为。 - 在路由中仅支持react-router-dom v6+ ## 安装 ```bash npm install keepalive-for-react ``` ```bash yarn add keepalive-for-react ``` ```bash pnpm add keepalive-for-react ``` ## 使用 ### 配合react-router-dom v6+ 或 react-router v7+使用 1. 安装react-router-dom v6+ 或 react-router v7+ ```bash # v6+ npm install react-router-dom keepalive-for-react keepalive-for-react-router@1.x.x # v7+ npm install react-router keepalive-for-react keepalive-for-react-router@2.x.x ``` 2. 在项目中使用KeepAlive ```tsx // v6+ keepalive-for-react-router@1.x.x // v7+ keepalive-for-react-router@2.x.x import KeepAliveRouteOutlet from "keepalive-for-react-router"; function Layout() { return (
); } ``` 或者 ```tsx import { useMemo } from "react"; import { useLocation } from "react-router-dom"; import { KeepAlive, useKeepAliveRef } from "keepalive-for-react"; function Layout() { const location = useLocation(); const aliveRef = useKeepAliveRef(); const outlet = useOutlet(); // 确定哪个路由组件处于活动状态 const currentCacheKey = useMemo(() => { return location.pathname + location.search; }, [location.pathname, location.search]); return (
}> {outlet}
); } ``` 详情请参见 [examples/react-router-dom-simple-starter](./examples/react-router-dom-simple-starter) [![在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) ### 在简单标签页中 ```bash npm install keepalive-for-react ``` ```tsx const tabs = [ { key: "tab1", label: "标签1", component: Tab1, }, { key: "tab2", label: "标签2", component: Tab2, }, { key: "tab3", label: "标签3", component: Tab3, }, ]; function App() { const [currentTab, setCurrentTab] = useState("tab1"); const tab = useMemo(() => { return tabs.find(tab => tab.key === currentTab); }, [currentTab]); return (
{/* ... */} {tab && }
); } ``` 详情请参见 [examples/simple-tabs-starter](./examples/simple-tabs-starter) [![在StackBlitz中打开](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/finedaybreak/keepalive-for-react/tree/main/examples/simple-tabs-starter) ## KeepAlive 属性 类型定义 ```tsx interface KeepAliveProps { // 确定哪个组件处于活动状态 activeCacheKey: string; children?: KeepAliveChildren; /** * 最大缓存数量 默认10 */ max?: number; exclude?: Array | string | RegExp; include?: Array | string | RegExp; onBeforeActive?: (activeCacheKey: string) => void; customContainerRef?: RefObject; cacheNodeClassName?: string; containerClassName?: string; errorElement?: ComponentType<{ children: ReactNode; }>; /** * 过渡效果 默认false */ transition?: boolean; /** * 使用view transition来过渡组件 默认false * @see https://developer.chrome.com/docs/web-platform/view-transitions/ */ viewTransition?: boolean; /** * 过渡时间 默认200ms */ duration?: number; aliveRef?: RefObject; /** * 缓存节点最大存活时间 (秒) * @default 0 (无限制) */ maxAliveTime?: number | MaxAliveConfig[]; /** * 启用 Activity 组件 from react 19.2+ * @default false * 注意: 如果启用 Activity 组件, useEffect 会在组件激活时触发 */ enableActivity?: boolean; } interface MaxAliveConfig { match: string | RegExp; expire: number; } ``` ## Hooks ### useEffectOnActive ```tsx useEffectOnActive(() => { console.log("active"); }, []); ``` ### useLayoutEffectOnActive ```tsx useLayoutEffectOnActive( () => { console.log("active"); }, [], false, ); // 第三个参数是可选的,默认为false, // 如果为true,表示在首次渲染时触发useLayoutEffect时会跳过回调 ``` ### useEffectOnCreate 只在组件首次创建(加入缓存)时执行一次回调,并在组件从缓存中被销毁时执行返回的清理函数。与 `useEffect(fn, [])` 不同,被缓存的组件再次激活时 **不会** 重新执行。 ```tsx useEffectOnCreate(() => { console.log("组件创建"); return () => { console.log("组件销毁"); }; }); ``` ### useLayoutEffectOnCreate 与 `useEffectOnCreate` 行为一致,内部使用 `useLayoutEffect`。适用于需要在浏览器绘制前同步执行创建逻辑的场景。 ```tsx useLayoutEffectOnCreate(() => { console.log("组件创建 (layout)"); return () => { console.log("组件销毁 (layout)"); }; }); ``` ### useKeepAliveContext 类型定义 ```ts interface KeepAliveContext { /** * 组件是否处于活动状态 */ active: boolean; /** * 刷新组件 * @param {string} [cacheKey] - 组件的缓存键。如果未提供,将刷新当前缓存的组件。 */ refresh: (cacheKey?: string) => void; /** * 销毁组件 * @param {string} [cacheKey] - 组件的缓存键,如果未提供,将销毁当前活动的缓存组件。 */ destroy: (cacheKey?: string | string[]) => Promise; /** * 销毁所有组件 */ destroyAll: () => Promise; /** * 销毁除提供的cacheKey外的其他组件 * @param {string} [cacheKey] - 组件的缓存键。如果未提供,将销毁除当前活动缓存组件外的所有组件。 */ destroyOther: (cacheKey?: string) => Promise; /** * 获取缓存节点 */ getCacheNodes: () => Array; } ``` ```tsx const { active, refresh, destroy, getCacheNodes } = useKeepAliveContext(); // active 是一个布尔值,true表示活动,false表示非活动 // refresh 是一个函数,你可以调用它来刷新组件 // destroy 是一个函数,你可以调用它来销毁组件 // ... // getCacheNodes 是一个函数,你可以调用它来获取缓存节点 ``` ### useKeepAliveRef 类型定义 ```ts interface KeepAliveRef { refresh: (cacheKey?: string) => void; destroy: (cacheKey?: string | string[]) => Promise; destroyAll: () => Promise; destroyOther: (cacheKey?: string) => Promise; getCacheNodes: () => Array; } ``` ```tsx function App() { const aliveRef = useKeepAliveRef(); // aliveRef.current 是一个 KeepAliveRef 对象 // 你可以在 aliveRef.current 上调用 refresh 和 destroy aliveRef.current?.refresh(); // 通常不需要手动调用 destroy,KeepAlive 会自动处理 aliveRef.current?.destroy(); return {/* ... */}; } // 或者 function AppRouter() { const aliveRef = useKeepAliveRef(); // aliveRef.current 是一个 KeepAliveRef 对象 // 你可以在 aliveRef.current 上调用 refresh 和 destroy aliveRef.current?.refresh(); aliveRef.current?.destroy(); return ; } ``` ## 开发 安装依赖 ```bash pnpm install ``` 构建包 ```bash pnpm build ``` [discord-link]: https://discord.gg/ycf896w7eA [discord-shield]: https://img.shields.io/discord/1232158668913381467?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square [discord-shield-badge]: https://img.shields.io/discord/1232158668913381467?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=for-the-badge ================================================ FILE: examples/react-router-dom-simple-starter/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: examples/react-router-dom-simple-starter/.npmrc ================================================ registry=https://registry.npmjs.org # 忽略 workspace 检查,作为独立项目运行 ignore-workspace-root-check=true ================================================ FILE: examples/react-router-dom-simple-starter/README.md ================================================ # React + TypeScript + Vite This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. Currently, two official plugins are available: - [@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 - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh ## Expanding the ESLint configuration If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: - Configure the top-level `parserOptions` property like this: ```js export default tseslint.config({ languageOptions: { // other options... parserOptions: { project: ['./tsconfig.node.json', './tsconfig.app.json'], tsconfigRootDir: import.meta.dirname, }, }, }) ``` - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` - Optionally add `...tseslint.configs.stylisticTypeChecked` - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: ```js // eslint.config.js import react from 'eslint-plugin-react' export default tseslint.config({ // Set the react version settings: { react: { version: '18.3' } }, plugins: { // Add the react plugin react, }, rules: { // other rules... // Enable its recommended rules ...react.configs.recommended.rules, ...react.configs['jsx-runtime'].rules, }, }) ``` ================================================ FILE: examples/react-router-dom-simple-starter/eslint.config.js ================================================ import js from '@eslint/js' import globals from 'globals' import reactHooks from 'eslint-plugin-react-hooks' import reactRefresh from 'eslint-plugin-react-refresh' import tseslint from 'typescript-eslint' export default tseslint.config( { ignores: ['dist'] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], files: ['**/*.{ts,tsx}'], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, plugins: { 'react-hooks': reactHooks, 'react-refresh': reactRefresh, }, rules: { ...reactHooks.configs.recommended.rules, 'react-refresh/only-export-components': [ 'warn', { allowConstantExport: true }, ], }, }, ) ================================================ FILE: examples/react-router-dom-simple-starter/index.html ================================================ Vite + React + TS
================================================ FILE: examples/react-router-dom-simple-starter/package.json ================================================ { "name": "react-router-dom-simple-starter", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview" }, "dependencies": { "keepalive-for-react": "^5.0.10", "keepalive-for-react-router": "^5.0.7", "react": "^19.2.5", "react-dom": "^19.2.5", "react-router": "^7.10.1" }, "devDependencies": { "@eslint/js": "^9.39.1", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^4.7.0", "autoprefixer": "^10.4.22", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^15.15.0", "postcss": "^8.5.6", "tailwindcss": "^3.4.18", "typescript": "^5.9.3", "typescript-eslint": "^8.48.1", "vite": "^6.4.1" }, "pnpm": { "onlyBuiltDependencies": [ "esbuild" ] } } ================================================ FILE: examples/react-router-dom-simple-starter/postcss.config.js ================================================ export default { plugins: { tailwindcss: {}, autoprefixer: {}, }, } ================================================ FILE: examples/react-router-dom-simple-starter/src/App.tsx ================================================ import { Fragment } from "react"; import { RouterProvider } from "react-router"; import router from "./router"; function App() { return ( ); } export default App; ================================================ FILE: examples/react-router-dom-simple-starter/src/index.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; /* keepalive-for-react animation example transition should be set true to enable animation and duration should be set to the animation duration */ .cache-component.active { animation: fadeIn 0.3s ease-in-out, slideIn 0.3s ease-in-out; } .cache-component.inactive { animation: fadeOut 0.3s ease-in-out, slideOut 0.3s ease-in-out; } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } } @keyframes slideIn { from { transform: translateX(-100%); } to { transform: translateX(0); } } @keyframes slideOut { from { transform: translateX(0); } to { transform: translateX(100%); } } ================================================ FILE: examples/react-router-dom-simple-starter/src/layout/index.tsx ================================================ import { useKeepAliveRef } from "keepalive-for-react"; import KeepAliveRouteOutlet from "keepalive-for-react-router"; import { ReactNode, Suspense, useEffect, useMemo, useRef } from "react"; import { Link, useLocation } from "react-router"; function Layout() { const location = useLocation(); const activePath = location.pathname + location.search; const aliveRef = useKeepAliveRef(); return (
Home About Counter Counter2 NestedA NestedB
); } // remember the scroll position of the page when switching routes function MemoScrollTopWrapper(props: { children?: ReactNode }) { const { children } = props; const domRef = useRef(null); const location = useLocation(); const scrollHistoryMap = useRef>(new Map()); const activeKey = useMemo(() => { return location.pathname + location.search; }, [location.pathname, location.search]); useEffect(() => { const divDom = domRef.current; if (!divDom) return; setTimeout(() => { divDom.scrollTo(0, scrollHistoryMap.current.get(activeKey) || 0); }, 300); // 300 milliseconds to wait for the animation transition ending const onScroll = (e: Event) => { const target = e.target as HTMLDivElement; if (!target) return; scrollHistoryMap.current.set(activeKey, target?.scrollTop || 0); }; divDom?.addEventListener("scroll", onScroll, { passive: true, }); return () => { divDom?.removeEventListener("scroll", onScroll); }; }, [activeKey]); return (
{children}
); } function CustomSuspense(props: { children: ReactNode }) { const { children } = props; return Loading...
}>{children}; } export default Layout; ================================================ FILE: examples/react-router-dom-simple-starter/src/main.tsx ================================================ import { createRoot } from "react-dom/client"; import App from "./App.tsx"; import "./index.css"; createRoot(document.getElementById("root")!).render(); ================================================ FILE: examples/react-router-dom-simple-starter/src/pages/about/index.tsx ================================================ function About() { return (

About

); } export default About; ================================================ FILE: examples/react-router-dom-simple-starter/src/pages/counter/index.tsx ================================================ import { useEffectOnActive, useEffectOnCreate, useKeepAliveContext } from "keepalive-for-react"; import { useState } from "react"; function Counter() { const [count, setCount] = useState(0); const { refresh, active } = useKeepAliveContext(); // useEffectOnActive(() => { // console.log("Counter is active (OnActive)", count); // return () => { // console.log("Counter is destroyed (OnActive)", count); // }; // }, [count]); useEffectOnCreate(() => { console.log("Counter is created (OnCreate)", count); return () => { console.log("Counter is destroyed (_OnCreate)", count); }; }); return (

Counter

Active: {active ? "true" : "false"}
{count}
); } export default Counter; ================================================ FILE: examples/react-router-dom-simple-starter/src/pages/home/index.tsx ================================================ import { useEffectOnActive } from "keepalive-for-react"; import { useRef } from "react"; function Home() { const domRef = useRef(null); useEffectOnActive( () => { console.log("Home is active"); const dom = domRef.current; if (dom) { // if transition is true, the dom size will be 0, because the dom is not rendered yet console.log(`home dom size: height ${dom.clientHeight}px width ${dom.clientWidth}px`); setTimeout(() => { console.log(`home dom size: height ${dom.clientHeight}px width ${dom.clientWidth}px`); }, 300); } }, [], true, ); return (

Home

Welcome to the home page, this is a simple example of how to use keepalive-for-react with react-router-dom.

Install

{`npm install keepalive-for-react react-router-dom`}
{"./src/layout/index.tsx"}
{``}
); } export default Home; ================================================ FILE: examples/react-router-dom-simple-starter/src/pages/nested/index.tsx ================================================ import { Outlet } from "react-router"; function Nested() { return (

Nested

); } export default Nested; ================================================ FILE: examples/react-router-dom-simple-starter/src/pages/nested/nested-a/index.tsx ================================================ function NestedA() { return (
NestedA

This is a nested route. It will be cached.

); } export default NestedA; ================================================ FILE: examples/react-router-dom-simple-starter/src/pages/nested/nested-b/index.tsx ================================================ function NestedB() { return (
NestedB

This is a nested route. It will be cached.

); } export default NestedB; ================================================ FILE: examples/react-router-dom-simple-starter/src/pages/nocache-counter/index.tsx ================================================ import { useEffectOnCreate, useKeepAliveContext, useLayoutEffectOnCreate } from "keepalive-for-react"; import { useState } from "react"; function NoCacheCounter() { const [count, setCount] = useState(0); const { refresh, destroy } = useKeepAliveContext(); useEffectOnCreate(() => { console.log("NoCacheCounter is created (OnCreate)", count); return () => { console.log("NoCacheCounter is destroyed (OnCreate)", count); }; }); useLayoutEffectOnCreate(() => { console.log("NoCacheCounter is created (OnLayoutCreate)", count); return () => { console.log("NoCacheCounter is destroyed (OnLayoutCreate)", count); }; }); return (

Counter (No Cache)

{count}
); } export default NoCacheCounter; ================================================ FILE: examples/react-router-dom-simple-starter/src/router/index.tsx ================================================ import { createBrowserRouter } from "react-router"; import Layout from "../layout"; import { lazy } from "react"; import Nested from "../pages/nested"; import NestedA from "../pages/nested/nested-a"; import NestedB from "../pages/nested/nested-b"; // import Home from "../pages/home"; // import About from "../pages/about"; // import Counter from "../pages/counter"; // import NoCacheCounter from "../pages/nocache-counter"; // lazy load const Home = lazy(() => import("../pages/home")); const About = lazy(() => import("../pages/about")); const Counter = lazy(() => import("../pages/counter")); const NoCacheCounter = lazy(() => import("../pages/nocache-counter")); const router = createBrowserRouter([ { path: "/", element: , children: [ { path: "", element: , }, { path: "/about", element: , }, { path: "/counter", element: , }, { path: "/nocache-counter", element: , }, { path: "/nested", element: , children: [ { path: "nested-a", element: , }, { path: "nested-b", element: , }, ], }, ], }, ]); export default router; ================================================ FILE: examples/react-router-dom-simple-starter/src/vite-env.d.ts ================================================ /// ================================================ FILE: examples/react-router-dom-simple-starter/tailwind.config.js ================================================ /** @type {import('tailwindcss').Config} */ export default { content: [ "./index.html", "./src/**/*.{js,ts,jsx,tsx}", ], theme: { extend: {}, }, plugins: [], } ================================================ FILE: examples/react-router-dom-simple-starter/tsconfig.app.json ================================================ { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", "noEmit": true, "jsx": "react-jsx", /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, "include": ["src"] } ================================================ FILE: examples/react-router-dom-simple-starter/tsconfig.app.tsbuildinfo ================================================ {"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"} ================================================ FILE: examples/react-router-dom-simple-starter/tsconfig.json ================================================ { "files": [], "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] } ================================================ FILE: examples/react-router-dom-simple-starter/tsconfig.node.json ================================================ { "compilerOptions": { "target": "ES2022", "lib": ["ES2023"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", "noEmit": true, /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, "include": ["vite.config.ts"] } ================================================ FILE: examples/react-router-dom-simple-starter/tsconfig.node.tsbuildinfo ================================================ {"root":["./vite.config.ts"],"version":"5.9.3"} ================================================ FILE: examples/react-router-dom-simple-starter/vite.config.ts ================================================ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; // import path from "node:path"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], // resolve: { // alias: { // "keepalive-for-react": path.resolve(__dirname, "../../packages/core/src/index.ts"), // "keepalive-for-react-router": path.resolve(__dirname, "../../packages/router/src/index.ts"), // }, // dedupe: ["react", "react-dom"], // }, }); ================================================ FILE: examples/simple-tabs-starter/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: examples/simple-tabs-starter/.npmrc ================================================ registry=https://registry.npmjs.org # 忽略 workspace 检查,作为独立项目运行 ignore-workspace-root-check=true ================================================ FILE: examples/simple-tabs-starter/README.md ================================================ # React + TypeScript + Vite This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. Currently, two official plugins are available: - [@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 - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh ## Expanding the ESLint configuration If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: - Configure the top-level `parserOptions` property like this: ```js export default tseslint.config({ languageOptions: { // other options... parserOptions: { project: ['./tsconfig.node.json', './tsconfig.app.json'], tsconfigRootDir: import.meta.dirname, }, }, }) ``` - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` - Optionally add `...tseslint.configs.stylisticTypeChecked` - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: ```js // eslint.config.js import react from 'eslint-plugin-react' export default tseslint.config({ // Set the react version settings: { react: { version: '18.3' } }, plugins: { // Add the react plugin react, }, rules: { // other rules... // Enable its recommended rules ...react.configs.recommended.rules, ...react.configs['jsx-runtime'].rules, }, }) ``` ================================================ FILE: examples/simple-tabs-starter/eslint.config.js ================================================ import js from '@eslint/js' import globals from 'globals' import reactHooks from 'eslint-plugin-react-hooks' import reactRefresh from 'eslint-plugin-react-refresh' import tseslint from 'typescript-eslint' export default tseslint.config( { ignores: ['dist'] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], files: ['**/*.{ts,tsx}'], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, plugins: { 'react-hooks': reactHooks, 'react-refresh': reactRefresh, }, rules: { ...reactHooks.configs.recommended.rules, 'react-refresh/only-export-components': [ 'warn', { allowConstantExport: true }, ], }, }, ) ================================================ FILE: examples/simple-tabs-starter/index.html ================================================ Vite + React + TS
================================================ FILE: examples/simple-tabs-starter/package.json ================================================ { "name": "simple-tabs-starter", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview" }, "dependencies": { "keepalive-for-react": "^5.0.8", "react": "^19.2.1", "react-dom": "^19.2.1", "zustand": "^5.0.9" }, "devDependencies": { "@eslint/js": "^9.39.1", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^4.7.0", "autoprefixer": "^10.4.22", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^15.15.0", "postcss": "^8.5.6", "tailwindcss": "^3.4.18", "typescript": "^5.9.3", "typescript-eslint": "^8.48.1", "vite": "^7.2.6" } } ================================================ FILE: examples/simple-tabs-starter/pnpm-workspace.yaml ================================================ # 这是一个独立项目,不属于父级 monorepo packages: [] ================================================ FILE: examples/simple-tabs-starter/postcss.config.js ================================================ export default { plugins: { tailwindcss: {}, autoprefixer: {}, }, } ================================================ FILE: examples/simple-tabs-starter/src/App.tsx ================================================ import { useEffect, useMemo, useState } from "react"; import { useEffectOnActive, useKeepAliveContext, KeepAlive } from "keepalive-for-react"; import useCounterStore from "./store/counter-store"; const tabs = [ { key: "tab1", label: "Tab 1", component: Tab1, }, { key: "tab2", label: "Tab 2", component: Tab2, }, { key: "tab3", label: "Tab 3", component: Tab3, }, ]; function App() { const [currentTab, setCurrentTab] = useState("tab1"); const tab = useMemo(() => { return tabs.find(tab => tab.key === currentTab); }, [currentTab]); const activeClass = "tab-item cursor-pointer text-blue-500"; const inactiveClass = "tab-item cursor-pointer"; return (
{tabs.map(tab => (
setCurrentTab(tab.key)} > {tab.label}
))}
{/* 虽然 Activity 可以提高性能,但是这里为了有淡出效果,禁用了19.2 的 Activity 组件 */} {tab && }
); } function Tab1() { const [text, setText] = useState("Hello KeepAlive for React"); const { refresh } = useKeepAliveContext(); const { count, increment, decrement } = useCounterStore(); useEffect(() => { console.log("Tab1 count", count); }, [count]); return (
Tab1

This is a demo for keepalive-for-react,
you can use it to keep the component alive when switching tabs.

{/* shared counter */}
Shared Counter
{count}
); } function Tab2() { const [count, setCount] = useState(0); const { refresh, active } = useKeepAliveContext(); const { count: sharedCount, increment: sharedIncrement, decrement: sharedDecrement } = useCounterStore(); useEffectOnActive(() => { console.log("Tab2 Counter is active", count); }, [count]); useEffect(() => { console.log("Tab2 sharedCount", sharedCount); }, [sharedCount]); return (
Tab2
Active: {active ? "true" : "false"}
{count}
{/* shared counter */}
Shared Counter
{sharedCount}
); } function Tab3() { const [count, setCount] = useState(0); const { refresh, active } = useKeepAliveContext(); useEffectOnActive(() => { console.log("Tab3 Counter is active", count, active); }, [count]); return (
Tab3 (no cache)
{count}
); } export default App; ================================================ FILE: examples/simple-tabs-starter/src/index.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; /* keepalive-for-react animation example transition should be set true to enable animation and duration should be set to the animation duration */ .cache-component.active { animation: fadeIn 0.3s ease-in-out, slideIn 0.3s ease-in-out; } .cache-component.inactive { animation: fadeOut 0.3s ease-in-out, slideOut 0.3s ease-in-out; } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } } @keyframes slideIn { from { transform: translateX(-100%); } to { transform: translateX(0); } } @keyframes slideOut { from { transform: translateX(0); } to { transform: translateX(100%); } } ================================================ FILE: examples/simple-tabs-starter/src/main.tsx ================================================ import { createRoot } from "react-dom/client"; import App from "./App.tsx"; import "./index.css"; createRoot(document.getElementById("root")!).render(); ================================================ FILE: examples/simple-tabs-starter/src/store/counter-store.tsx ================================================ import { create } from "zustand"; export const useCounterStore = create(set => ({ count: 0, increment: () => set(state => ({ count: state.count + 1 })), decrement: () => set(state => ({ count: state.count - 1 })), })); interface CounterStore { count: number; increment: () => void; decrement: () => void; } export default useCounterStore; ================================================ FILE: examples/simple-tabs-starter/src/vite-env.d.ts ================================================ /// ================================================ FILE: examples/simple-tabs-starter/tailwind.config.js ================================================ /** @type {import('tailwindcss').Config} */ export default { content: [ "./index.html", "./src/**/*.{js,ts,jsx,tsx}", ], theme: { extend: {}, }, plugins: [], } ================================================ FILE: examples/simple-tabs-starter/tsconfig.app.json ================================================ { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", "noEmit": true, "jsx": "react-jsx", /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, "include": ["src"] } ================================================ FILE: examples/simple-tabs-starter/tsconfig.app.tsbuildinfo ================================================ {"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/store/counter-store.tsx"],"version":"5.9.3"} ================================================ FILE: examples/simple-tabs-starter/tsconfig.json ================================================ { "files": [], "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] } ================================================ FILE: examples/simple-tabs-starter/tsconfig.node.json ================================================ { "compilerOptions": { "target": "ES2022", "lib": ["ES2023"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", "noEmit": true, /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, "include": ["vite.config.ts"] } ================================================ FILE: examples/simple-tabs-starter/tsconfig.node.tsbuildinfo ================================================ {"root":["./vite.config.ts"],"version":"5.9.3"} ================================================ FILE: examples/simple-tabs-starter/vite.config.ts ================================================ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], }); ================================================ FILE: package.json ================================================ { "name": "keepalive-for-react-monorepo", "version": "0.0.0", "private": true, "description": "A react component like in vue", "type": "module", "scripts": { "build": "pnpm -r build", "clean": "pnpm -r clean", "format": "prettier --write \"**/*.{ts,tsx,json}\"", "changeset": "changeset", "version": "changeset version", "release": "pnpm build && changeset publish", "prepare": "husky", "example:router": "cd examples/react-router-dom-simple-starter && pnpm install && pnpm dev", "example:tabs": "cd examples/simple-tabs-starter && pnpm install && pnpm dev" }, "lint-staged": { "*.{ts,tsx,json,md}": "prettier --write" }, "author": "wongyichen", "license": "MIT", "devDependencies": { "@changesets/cli": "^2.30.0", "@types/node": "^20.8.2", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "husky": "^9.0.11", "lint-staged": "^15.2.2", "prettier": "^3.0.3", "tsup": "^8.5.1", "typescript": "^5.2.2" }, "engines": { "node": ">=18", "pnpm": ">=8" }, "packageManager": "pnpm@10.24.0", "pnpm": { "onlyBuiltDependencies": [ "esbuild" ] } } ================================================ FILE: packages/core/CHANGELOG.md ================================================ # keepalive-for-react ## 5.0.11 ### Patch Changes - fix destroy emit ## 5.0.10 ### Patch Changes - new hooks: useEffectOnCreate, useLayoutEffectOnCreate ## 5.0.9 ### Patch Changes - Wraps Activity with a delayed hide so the outgoing component stays visible during the transition duration, preserving fade/transition effects when enableActivity is on. ## 5.0.8 ### Patch Changes - fix(CacheComponent): switch useEffect to useLayoutEffect for DOM operations ## 5.0.7 ### Patch Changes - change enableActivity to default false ## 5.0.6 ### Patch Changes - update doc ## 5.0.4 ### Patch Changes - bug fixes - bux fix - bug fixes - bug fixes - bug fix - Bug fixes - bug fixes - bug fixes - bug fixes ## 5.0.4-beta.8 ### Patch Changes - bug fixes ## 5.0.4-beta.7 ### Patch Changes - bug fixes ## 5.0.4-beta.6 ### Patch Changes - bug fixes ## 5.0.4-beta.5 ### Patch Changes - bug fixes ## 5.0.4-beta.4 ### Patch Changes - bug fixes ## 5.0.4-beta.3 ### Patch Changes - bug fixes ## 5.0.4-beta.1 ### Patch Changes - bug fixes ## 5.0.4-beta.0 ### Patch Changes - Bug fixes ================================================ FILE: packages/core/README.md ================================================

keepalive-for-react logo

KeepAlive for React

A React KeepAlive component like keep-alive in vue

[中文](./README.zh_CN.md) | English [![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]
## Packages | Package | Version | Description | | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | | [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 | | [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 | ## Features - Support react-router-dom v6+ or react-router v7+ - Support React v16+ ~ v18+ (v19.2 Activity component support [v5.0.0]) - Support Suspense and Lazy import - Support ErrorBoundary - Support Custom Container - Support Switching Animation Transition with className `active` and `inactive` - Simply implement, without any extra dependencies and hacking ways - Only 6KB minified size - Support interrupt state effect when component is not active (v5.0.0) ## Attention - **Version Compatibility**: - For React 18, please use `keepalive-for-react@4.x.x` - For React 19.2+, please use `keepalive-for-react@5.x.x` - DO NOT use , it CANNOT work with keepalive-for-react in development mode. because it can lead to some unexpected behavior. - In Router only support react-router-dom v6+ ## Install ```bash npm install keepalive-for-react ``` ```bash yarn add keepalive-for-react ``` ```bash pnpm add keepalive-for-react ``` ## Usage ### in react-router-dom v6+ or react-router v7+ 1. install react-router-dom v6+ or react-router v7+ ```bash # v6+ npm install react-router-dom keepalive-for-react keepalive-for-react-router@1.x.x # v7+ npm install react-router keepalive-for-react keepalive-for-react-router@2.x.x ``` 2. use KeepAlive in your project ```tsx // v6+ keepalive-for-react-router@1.x.x // v7+ keepalive-for-react-router@2.x.x import KeepAliveRouteOutlet from "keepalive-for-react-router"; function Layout() { return (
); } ``` or ```tsx import { useMemo } from "react"; // v6+ import { useLocation, useOutlet } from "react-router-dom"; // v7 // import { useLocation, useOutlet } from "react-router"; import { KeepAlive, useKeepAliveRef } from "keepalive-for-react"; function Layout() { const location = useLocation(); const aliveRef = useKeepAliveRef(); const outlet = useOutlet(); // determine which route component to is active const currentCacheKey = useMemo(() => { return location.pathname + location.search; }, [location.pathname, location.search]); return (
}> {outlet}
); } ``` details see [examples/react-router-dom-simple-starter](./examples/react-router-dom-simple-starter) [![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) ### in simple tabs ```bash npm install keepalive-for-react ``` ```tsx const tabs = [ { key: "tab1", label: "Tab 1", component: Tab1, }, { key: "tab2", label: "Tab 2", component: Tab2, }, { key: "tab3", label: "Tab 3", component: Tab3, }, ]; function App() { const [currentTab, setCurrentTab] = useState("tab1"); const tab = useMemo(() => { return tabs.find(tab => tab.key === currentTab); }, [currentTab]); return (
{/* ... */} {tab && }
); } ``` details see [examples/simple-tabs-starter](./examples/simple-tabs-starter) [![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) ## KeepAlive Props type definition ```tsx interface KeepAliveProps { // determine which component to is active activeCacheKey: string; children?: KeepAliveChildren; /** * max cache count default 10 */ max?: number; exclude?: Array | string | RegExp; include?: Array | string | RegExp; onBeforeActive?: (activeCacheKey: string) => void; customContainerRef?: RefObject; cacheNodeClassName?: string; containerClassName?: string; errorElement?: ComponentType<{ children: ReactNode; }>; /** * transition default false */ transition?: boolean; /** * use view transition to animate the component when switching tabs * @see https://developer.chrome.com/docs/web-platform/view-transitions/ */ viewTransition?: boolean; /** * transition duration default 200 */ duration?: number; aliveRef?: RefObject; /** * max alive time for cache node (second) * @default 0 (no limit) */ maxAliveTime?: number | MaxAliveConfig[]; /** * enable Activity component from react 19+ * @default false * Activity component can improve performance * Attention: if enable Activity component, useEffect will trigger when the component is active */ enableActivity?: boolean; } interface MaxAliveConfig { match: string | RegExp; expire: number; } ``` ## Hooks ### useEffectOnActive ```tsx useEffectOnActive(() => { console.log("active"); }, []); ``` ### useLayoutEffectOnActive ```tsx useLayoutEffectOnActive( () => { console.log("active"); }, [], false, ); // the third parameter is optional, default is false, // if true, which means the callback will be skipped when the useLayoutEffect is triggered in first render ``` ### useEffectOnCreate Run 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. ```tsx useEffectOnCreate(() => { console.log("component created"); return () => { console.log("component destroyed"); }; }); ``` ### useLayoutEffectOnCreate Same as `useEffectOnCreate` but uses `useLayoutEffect` internally. Useful when the create-time logic needs to run synchronously before the browser paints. ```tsx useLayoutEffectOnCreate(() => { console.log("component created (layout)"); return () => { console.log("component destroyed (layout)"); }; }); ``` ### useKeepAliveContext type definition ```ts interface KeepAliveContext { /** * whether the component is active */ active: boolean; /** * refresh the component * @param {string} [cacheKey] - The cache key of the component. If not provided, the current cached component will be refreshed. */ refresh: (cacheKey?: string) => void; /** * destroy the component * @param {string} [cacheKey] - the cache key of the component, if not provided, current active cached component will be destroyed */ destroy: (cacheKey?: string | string[]) => Promise; /** * destroy all components */ destroyAll: () => Promise; /** * destroy other components except the provided cacheKey * @param {string} [cacheKey] - The cache key of the component. If not provided, destroy all components except the current active cached component. */ destroyOther: (cacheKey?: string) => Promise; /** * get the cache nodes */ getCacheNodes: () => Array; } ``` ```tsx const { active, refresh, destroy, getCacheNodes } = useKeepAliveContext(); // active is a boolean, true is active, false is inactive // refresh is a function, you can call it to refresh the component // destroy is a function, you can call it to destroy the component // ... // getCacheNodes is a function, you can call it to get the cache nodes ``` ### useKeepAliveRef type definition ```ts interface KeepAliveRef { refresh: (cacheKey?: string) => void; destroy: (cacheKey?: string | string[]) => Promise; destroyAll: () => Promise; destroyOther: (cacheKey?: string) => Promise; getCacheNodes: () => Array; } ``` ```tsx function App() { const aliveRef = useKeepAliveRef(); // aliveRef.current is a KeepAliveRef object // you can call refresh and destroy on aliveRef.current aliveRef.current?.refresh(); // it is not necessary to call destroy manually, KeepAlive will handle it automatically aliveRef.current?.destroy(); return {/* ... */}; } // or function AppRouter() { const aliveRef = useKeepAliveRef(); // aliveRef.current is a KeepAliveRef object // you can call refresh and destroy on aliveRef.current aliveRef.current?.refresh(); aliveRef.current?.destroy(); return ; } ``` ## Development install dependencies ```bash pnpm install ``` build package ```bash pnpm build ``` [discord-link]: https://discord.gg/ycf896w7eA [discord-shield]: https://img.shields.io/discord/1232158668913381467?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square [discord-shield-badge]: https://img.shields.io/discord/1232158668913381467?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=for-the-badge ================================================ FILE: packages/core/README.zh_CN.md ================================================

keepalive-for-react logo

React KeepAlive 组件

一个类似Vue中keep-alive的React KeepAlive组件

[English](./README.md) | 中文 [![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) ## 包信息 | 包名 | 版本 | 描述 | | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | [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功能 | | [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集成 | ## 特性 - 支持react-router-dom v6+ 或 react-router v7+ - 支持React v16+ ~ v18+ (v19.2 Activity component support [v5.0.0]) - 支持Suspense和懒加载导入 - 支持错误边界 - 支持自定义容器 - 支持使用className `active`和`inactive`进行切换动画过渡 - 简单实现,无需任何额外依赖和hack方式 - 压缩后仅6KB大小 - 支持中断state Effect当组件不活动时 (v5.0.0) ## 注意事项 - **版本兼容性**: - React 18 请使用 `keepalive-for-react@4.x.x` - React 19.2+ 请使用 `keepalive-for-react@5.x.x` - 请勿使用 ,它在开发模式下无法与keepalive-for-react一起工作。因为它可能会导致一些意外行为。 - 在路由中仅支持react-router-dom v6+ ## 安装 ```bash npm install keepalive-for-react ``` ```bash yarn add keepalive-for-react ``` ```bash pnpm add keepalive-for-react ``` ## 使用 ### 配合react-router-dom v6+ 或 react-router v7+使用 1. 安装react-router-dom v6+ 或 react-router v7+ ```bash # v6+ npm install react-router-dom keepalive-for-react keepalive-for-react-router@1.x.x # v7+ npm install react-router keepalive-for-react keepalive-for-react-router@2.x.x ``` 2. 在项目中使用KeepAlive ```tsx // v6+ keepalive-for-react-router@1.x.x // v7+ keepalive-for-react-router@2.x.x import KeepAliveRouteOutlet from "keepalive-for-react-router"; function Layout() { return (
); } ``` 或者 ```tsx import { useMemo } from "react"; import { useLocation } from "react-router-dom"; import { KeepAlive, useKeepAliveRef } from "keepalive-for-react"; function Layout() { const location = useLocation(); const aliveRef = useKeepAliveRef(); const outlet = useOutlet(); // 确定哪个路由组件处于活动状态 const currentCacheKey = useMemo(() => { return location.pathname + location.search; }, [location.pathname, location.search]); return (
}> {outlet}
); } ``` 详情请参见 [examples/react-router-dom-simple-starter](./examples/react-router-dom-simple-starter) [![在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) ### 在简单标签页中 ```bash npm install keepalive-for-react ``` ```tsx const tabs = [ { key: "tab1", label: "标签1", component: Tab1, }, { key: "tab2", label: "标签2", component: Tab2, }, { key: "tab3", label: "标签3", component: Tab3, }, ]; function App() { const [currentTab, setCurrentTab] = useState("tab1"); const tab = useMemo(() => { return tabs.find(tab => tab.key === currentTab); }, [currentTab]); return (
{/* ... */} {tab && }
); } ``` 详情请参见 [examples/simple-tabs-starter](./examples/simple-tabs-starter) [![在StackBlitz中打开](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/finedaybreak/keepalive-for-react/tree/main/examples/simple-tabs-starter) ## KeepAlive 属性 类型定义 ```tsx interface KeepAliveProps { // 确定哪个组件处于活动状态 activeCacheKey: string; children?: KeepAliveChildren; /** * 最大缓存数量 默认10 */ max?: number; exclude?: Array | string | RegExp; include?: Array | string | RegExp; onBeforeActive?: (activeCacheKey: string) => void; customContainerRef?: RefObject; cacheNodeClassName?: string; containerClassName?: string; errorElement?: ComponentType<{ children: ReactNode; }>; /** * 过渡效果 默认false */ transition?: boolean; /** * 使用view transition来过渡组件 默认false * @see https://developer.chrome.com/docs/web-platform/view-transitions/ */ viewTransition?: boolean; /** * 过渡时间 默认200ms */ duration?: number; aliveRef?: RefObject; /** * 缓存节点最大存活时间 (秒) * @default 0 (无限制) */ maxAliveTime?: number | MaxAliveConfig[]; /** * enable Activity component from react 19+ * @default false * Activity component can improve performance * Attention: if enable Activity component, useEffect will trigger when the component is active */ enableActivity?: boolean; } interface MaxAliveConfig { match: string | RegExp; expire: number; } ``` ## Hooks ### useEffectOnActive ```tsx useEffectOnActive(() => { console.log("active"); }, []); ``` ### useLayoutEffectOnActive ```tsx useLayoutEffectOnActive( () => { console.log("active"); }, [], false, ); // 第三个参数是可选的,默认为false, // 如果为true,表示在首次渲染时触发useLayoutEffect时会跳过回调 ``` ### useEffectOnCreate 只在组件首次创建(加入缓存)时执行一次回调,并在组件从缓存中被销毁时执行返回的清理函数。与 `useEffect(fn, [])` 不同,被缓存的组件再次激活时 **不会** 重新执行。 ```tsx useEffectOnCreate(() => { console.log("组件创建"); return () => { console.log("组件销毁"); }; }); ``` ### useLayoutEffectOnCreate 与 `useEffectOnCreate` 行为一致,内部使用 `useLayoutEffect`。适用于需要在浏览器绘制前同步执行创建逻辑的场景。 ```tsx useLayoutEffectOnCreate(() => { console.log("组件创建 (layout)"); return () => { console.log("组件销毁 (layout)"); }; }); ``` ### useKeepAliveContext 类型定义 ```ts interface KeepAliveContext { /** * 组件是否处于活动状态 */ active: boolean; /** * 刷新组件 * @param {string} [cacheKey] - 组件的缓存键。如果未提供,将刷新当前缓存的组件。 */ refresh: (cacheKey?: string) => void; /** * 销毁组件 * @param {string} [cacheKey] - 组件的缓存键,如果未提供,将销毁当前活动的缓存组件。 */ destroy: (cacheKey?: string | string[]) => Promise; /** * 销毁所有组件 */ destroyAll: () => Promise; /** * 销毁除提供的cacheKey外的其他组件 * @param {string} [cacheKey] - 组件的缓存键。如果未提供,将销毁除当前活动缓存组件外的所有组件。 */ destroyOther: (cacheKey?: string) => Promise; /** * 获取缓存节点 */ getCacheNodes: () => Array; } ``` ```tsx const { active, refresh, destroy, getCacheNodes } = useKeepAliveContext(); // active 是一个布尔值,true表示活动,false表示非活动 // refresh 是一个函数,你可以调用它来刷新组件 // destroy 是一个函数,你可以调用它来销毁组件 // ... // getCacheNodes 是一个函数,你可以调用它来获取缓存节点 ``` ### useKeepAliveRef 类型定义 ```ts interface KeepAliveRef { refresh: (cacheKey?: string) => void; destroy: (cacheKey?: string | string[]) => Promise; destroyAll: () => Promise; destroyOther: (cacheKey?: string) => Promise; getCacheNodes: () => Array; } ``` ```tsx function App() { const aliveRef = useKeepAliveRef(); // aliveRef.current 是一个 KeepAliveRef 对象 // 你可以在 aliveRef.current 上调用 refresh 和 destroy aliveRef.current?.refresh(); // 通常不需要手动调用 destroy,KeepAlive 会自动处理 aliveRef.current?.destroy(); return {/* ... */}; } // 或者 function AppRouter() { const aliveRef = useKeepAliveRef(); // aliveRef.current 是一个 KeepAliveRef 对象 // 你可以在 aliveRef.current 上调用 refresh 和 destroy aliveRef.current?.refresh(); aliveRef.current?.destroy(); return ; } ``` ## 开发 安装依赖 ```bash pnpm install ``` 构建包 ```bash pnpm build ``` 链接包到全局 ```bash pnpm link --global ``` 在演示项目中测试 ```bash cd demo pnpm link --global keepalive-for-react ``` [discord-link]: https://discord.gg/ycf896w7eA [discord-shield]: https://img.shields.io/discord/1232158668913381467?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square [discord-shield-badge]: https://img.shields.io/discord/1232158668913381467?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=for-the-badge ================================================ FILE: packages/core/package.json ================================================ { "name": "keepalive-for-react", "version": "5.0.11", "description": "A react component like in vue", "homepage": "https://github.com/finedaybreak/keepalive-for-react", "repository": { "type": "git", "url": "git+https://github.com/finedaybreak/keepalive-for-react.git" }, "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.mjs", "require": "./dist/index.cjs" } }, "main": "./dist/index.cjs", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", "type": "module", "files": [ "dist" ], "scripts": { "build": "tsup", "clean": "rm -rf dist" }, "keywords": [ "keepalive", "keep-alive", "react keepalive", "keepalive for react", "keepalive-for-react" ], "author": "wongyichen", "license": "MIT", "peerDependencies": { "react": ">=16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": ">=16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "dependencies": { "mitt": "^3.0.1" } } ================================================ FILE: packages/core/src/compat/Activity.tsx ================================================ import * as React from "react"; // React 19+ 原生 Activity 组件 const NativeActivity = (React as any).Activity; export interface ActivityProps { children: React.ReactNode; mode: "visible" | "hidden"; } /** * Activity 兼容组件 * - React 19+: 使用原生 Activity * - React 18: 使用 Fragment fallback(用户自行处理隐藏逻辑) */ export const Activity: React.ComponentType = NativeActivity ?? React.Fragment; export const hasNativeActivity = !!NativeActivity; ================================================ FILE: packages/core/src/compat/safeStartTransition.ts ================================================ import { startTransition as reactStartTransition } from "react"; import { isFn } from "../utils"; /** * Compatible with React versions < 18 startTransition * @param cb Callback function to be executed in transition */ const safeStartTransition = (cb: () => void): void => { if (typeof reactStartTransition !== "undefined" && isFn(reactStartTransition)) { reactStartTransition(cb); } else { cb(); } }; export default safeStartTransition; ================================================ FILE: packages/core/src/components/CacheComponent/index.tsx ================================================ import { ComponentType, Fragment, memo, ReactNode, RefObject, useLayoutEffect, useMemo, useRef } from "react"; import { hasNativeActivity } from "../../compat/Activity"; import { createPortal } from "react-dom"; import { delayAsync, domAttrSet, isInclude } from "../../utils"; import MemoizedActivty from "../MemoizedActivty"; export interface CacheComponentProps { children: ReactNode; errorElement?: ComponentType<{ children: ReactNode; }>; containerDivRef: RefObject; cacheNodeClassName: string; renderCount: number; active: boolean; cacheKey: string; transition: boolean; viewTransition: boolean; duration: number; exclude?: Array | string | RegExp; include?: Array | string | RegExp; destroy: (cacheKey: string | string[]) => Promise; enableActivity: boolean; } const cacheDivMarkedClassName = "keepalive-cache-div"; function getChildNodes(dom?: HTMLDivElement) { return dom ? Array.from(dom.children) : []; } function removeDivNodes(nodes: Element[]) { nodes.forEach(node => { if (node.classList.contains(cacheDivMarkedClassName)) { node.remove(); } }); } function renderCacheDiv(containerDiv: HTMLDivElement, cacheDiv: HTMLDivElement) { const removeNodes = getChildNodes(containerDiv); removeDivNodes(removeNodes); containerDiv.appendChild(cacheDiv); cacheDiv.classList.remove("inactive"); cacheDiv.classList.add("active"); } function switchActiveNodesToInactive(containerDiv: HTMLDivElement, cacheKey: string) { const nodes = getChildNodes(containerDiv); const activeNodes = nodes.filter(node => node.classList.contains("active") && node.getAttribute("data-cache-key") !== cacheKey); activeNodes.forEach(node => { node.classList.remove("active"); node.classList.add("inactive"); }); return activeNodes; } function isCached( cacheKey: string, exclude?: Array | string | RegExp, include?: Array | string | RegExp, ) { if (include) { return isInclude(include, cacheKey); } else { if (exclude) { return !isInclude(exclude, cacheKey); } return true; } } const CacheComponent = memo( function (props: CacheComponentProps): any { const { errorElement: ErrorBoundary = Fragment, cacheNodeClassName, children, cacheKey, exclude, include, enableActivity } = props; const { active, renderCount, destroy, transition, viewTransition, duration, containerDivRef } = props; const activatedRef = useRef(false); activatedRef.current = activatedRef.current || active; const cacheDiv = useMemo(() => { const cacheDiv = document.createElement("div"); domAttrSet(cacheDiv) .set("data-cache-key", cacheKey) .set("style", "height: 100%") .set("data-render-count", renderCount.toString()); cacheDiv.className = cacheDivMarkedClassName + (cacheNodeClassName ? ` ${cacheNodeClassName}` : ""); return cacheDiv; }, [renderCount, cacheNodeClassName]); useLayoutEffect(() => { const cached = isCached(cacheKey, exclude, include); const containerDiv = containerDivRef.current; if (!containerDiv) { console.warn(`keepalive: cache container not found`); return; } if (transition) { (async () => { if (active) { const inactiveNodes = switchActiveNodesToInactive(containerDiv, cacheKey); // duration - 40ms is to avoid the animation effect ending too early await delayAsync(duration - 40); removeDivNodes(inactiveNodes); if (containerDiv.contains(cacheDiv)) { return; } renderCacheDiv(containerDiv, cacheDiv); } else { if (!cached) { await delayAsync(duration); destroy(cacheKey); } } })(); } else { if (active) { const makeChange = () => { const inactiveNodes = switchActiveNodesToInactive(containerDiv, cacheKey); removeDivNodes(inactiveNodes); if (containerDiv.contains(cacheDiv)) { return; } renderCacheDiv(containerDiv, cacheDiv); }; if (viewTransition && (document as any).startViewTransition) { (document as any).startViewTransition(makeChange); } else { makeChange(); } } else { if (!cached) { destroy(cacheKey); } } } }, [active, containerDivRef, cacheKey, exclude, include]); return activatedRef.current ? createPortal( {hasNativeActivity && enableActivity ? ( {children} ) : ( children )} , cacheDiv, cacheKey, ) : null; }, (prevProps, nextProps) => { return ( prevProps.active === nextProps.active && prevProps.renderCount === nextProps.renderCount && prevProps.children === nextProps.children && prevProps.exclude === nextProps.exclude && prevProps.include === nextProps.include ); }, ); export default CacheComponent; ================================================ FILE: packages/core/src/components/CacheComponentProvider/index.tsx ================================================ import { memo, ReactNode, useMemo } from "react"; import { CacheComponentContext, KeepAliveContext } from "../CacheContext"; interface CacheComponentProviderProps extends KeepAliveContext { children?: ReactNode; } const CacheComponentProvider = memo(function (props: CacheComponentProviderProps) { const { children, active, refresh, destroy, destroyAll, destroyOther, getCacheNodes, _cacheKey } = props; const value = useMemo(() => { return { active, refresh, destroy, destroyAll, destroyOther, getCacheNodes, _cacheKey }; }, [active, refresh, destroy, destroyAll, destroyOther, getCacheNodes, _cacheKey]); return {children}; }); export default CacheComponentProvider; ================================================ FILE: packages/core/src/components/CacheContext/index.tsx ================================================ import { createContext } from "react"; import { KeepAliveAPI } from "../KeepAlive"; export interface KeepAliveContext extends KeepAliveAPI { /** * whether the component is active */ active: boolean; /** * the cache key of the component */ _cacheKey: string; } export const CacheComponentContext = createContext({ active: false, _cacheKey: "", refresh: () => {}, destroy: () => Promise.resolve(), destroyAll: () => Promise.resolve(), destroyOther: () => Promise.resolve(), getCacheNodes: () => [], }); ================================================ FILE: packages/core/src/components/KeepAlive/index.tsx ================================================ import { ComponentType, Fragment, ReactElement, ReactNode, RefObject, useCallback, useImperativeHandle, useLayoutEffect, useRef, useState, } from "react"; import { isArr, isFn, isNil, isRegExp, macroTask } from "../../utils"; import CacheComponentProvider from "../CacheComponentProvider"; import CacheComponent from "../CacheComponent"; import safeStartTransition from "../../compat/safeStartTransition"; import eventBus from "../../event"; export type KeepAliveChildren = ReactNode | ReactElement | null | undefined; export interface KeepAliveProps { activeCacheKey: string; children?: KeepAliveChildren; /** * max cache count default 10 */ max?: number; exclude?: Array | string | RegExp; include?: Array | string | RegExp; onBeforeActive?: (activeCacheKey: string) => void; customContainerRef?: RefObject | undefined; cacheNodeClassName?: string; containerClassName?: string; errorElement?: ComponentType<{ children: ReactNode; }>; /** * transition default false */ transition?: boolean; /** * view transition default false * * use viewTransition to animate the component when switching tabs * * @see https://developer.chrome.com/docs/web-platform/view-transitions/ */ viewTransition?: boolean; /** * transition duration default 200 */ duration?: number; aliveRef?: RefObject; /** * max alive time for cache node (second) * @default 0 (no limit) */ maxAliveTime?: number | MaxAliveConfig[]; /** * enable Activity component from react 19+ * @default false * Activity component can improve performance, but it will affect the transition effect * Attention: if enable Activity component, useEffect will trigger when the component is active */ enableActivity?: boolean; } interface MaxAliveConfig { match: string | RegExp; expire: number; } export interface CacheNode { cacheKey: string; ele?: KeepAliveChildren; lastActiveTime: number; renderCount: number; } export interface KeepAliveAPI { /** * Refreshes the component. * @param {string} [cacheKey] - The cache key of the component. If not provided, the current cached component will be refreshed. */ refresh: (cacheKey?: string) => void; /** * destroy the component * @param {string} [cacheKey] - the cache key of the component, if not provided, current active cached component will be destroyed */ destroy: (cacheKey?: string | string[]) => Promise; /** * destroy all components */ destroyAll: () => Promise; /** * destroy other components except the provided cacheKey * @param {string} [cacheKey] - The cache key of the component. If not provided, destroy all components except the current active cached component. */ destroyOther: (cacheKey?: string) => Promise; /** * get the cache nodes */ getCacheNodes: () => Array; } export interface KeepAliveRef extends KeepAliveAPI {} export function useKeepAliveRef() { return useRef(null); } function KeepAlive(props: KeepAliveProps) { const { activeCacheKey, max = 10, exclude, include, onBeforeActive, customContainerRef, cacheNodeClassName = `cache-component`, containerClassName = "keep-alive-render", errorElement, transition = false, viewTransition = false, duration = 200, children, aliveRef, maxAliveTime = 0, enableActivity = false, } = props; const containerDivRef = customContainerRef || useRef(null); const [cacheNodes, setCacheNodes] = useState>([]); useLayoutEffect(() => { if (isNil(activeCacheKey)) return; safeStartTransition(() => { setCacheNodes(prevCacheNodes => { const lastActiveTime = Date.now(); const cacheNode = prevCacheNodes.find(item => item.cacheKey === activeCacheKey); if (cacheNode) { return prevCacheNodes.map(item => { if (item.cacheKey === activeCacheKey) { let needUpdate = false; if (isFn(onBeforeActive)) onBeforeActive(activeCacheKey); if (maxAliveTime) { const prev = item.lastActiveTime; if (isArr(maxAliveTime)) { const config = maxAliveTime.find(item => { return isRegExp(item.match) ? item.match.test(activeCacheKey) : item.match === activeCacheKey; }); if (config) { needUpdate = config && prev + config.expire * 1000 < lastActiveTime; } } else { needUpdate = prev + maxAliveTime * 1000 < lastActiveTime; } } if (needUpdate) { eventBus.emit("destroy", [activeCacheKey]); } return { ...item, ele: children, lastActiveTime, renderCount: needUpdate ? item.renderCount + 1 : item.renderCount, }; } return item; }); } else { if (isFn(onBeforeActive)) onBeforeActive(activeCacheKey); if (prevCacheNodes.length > max) { const node = prevCacheNodes.reduce((prev, cur) => { return prev.lastActiveTime < cur.lastActiveTime ? prev : cur; }); const deletedNodes = prevCacheNodes.splice(prevCacheNodes.indexOf(node), 1); const deletedCacheKeys = deletedNodes.map(item => item.cacheKey); eventBus.emit("destroy", deletedCacheKeys); } return [...prevCacheNodes, { cacheKey: activeCacheKey, lastActiveTime, ele: children, renderCount: 0 }]; } }); }); }, [activeCacheKey, children]); const refresh = useCallback( (cacheKey?: string) => { setCacheNodes(cacheNodes => { const targetCacheKey = cacheKey || activeCacheKey; eventBus.emit("refresh", targetCacheKey); return cacheNodes.map(item => { if (item.cacheKey === targetCacheKey) { return { ...item, renderCount: item.renderCount + 1 }; } return item; }); }); }, [setCacheNodes, activeCacheKey], ); const destroy = useCallback( (cacheKey?: string | string[]) => { const targetCacheKey = cacheKey || activeCacheKey; const cacheKeys = isArr(targetCacheKey) ? targetCacheKey : [targetCacheKey]; eventBus.emit("destroy", cacheKeys); return new Promise(resolve => { macroTask(() => { setCacheNodes(cacheNodes => { return [...cacheNodes.filter(item => !cacheKeys.includes(item.cacheKey))]; }); resolve(); }); }); }, [setCacheNodes, activeCacheKey], ); const destroyAll = useCallback(() => { return new Promise(resolve => { eventBus.emit("destroyAll"); macroTask(() => { setCacheNodes([]); resolve(); }); }); }, [setCacheNodes]); const destroyOther = useCallback( (cacheKey?: string) => { const targetCacheKey = cacheKey || activeCacheKey; return new Promise(resolve => { eventBus.emit("destroyOther", targetCacheKey); macroTask(() => { setCacheNodes(cacheNodes => { return [...cacheNodes.filter(item => item.cacheKey === targetCacheKey)]; }); resolve(); }); }); }, [activeCacheKey, setCacheNodes], ); const getCacheNodes = useCallback(() => { return cacheNodes; }, [cacheNodes]); useImperativeHandle(aliveRef, () => ({ refresh, destroy, destroyAll, destroyOther, getCacheNodes, })); return (
{cacheNodes.map(item => { const { cacheKey, ele, renderCount } = item; return ( {ele} ); })}
); } export default KeepAlive; ================================================ FILE: packages/core/src/components/MemoizedActivty/index.tsx ================================================ import { Activity } from "../../compat/Activity"; import { ReactNode, useLayoutEffect, useRef, useState, memo, startTransition } from "react"; interface MemoizedActivtyProps { children: ReactNode; active: boolean; duration: number; } function _MemoizedActivty({ children, active, duration }: MemoizedActivtyProps) { const [delayedActive, setDelayedActive] = useState(active); const delayedActiveTimerRef = useRef>(null); useLayoutEffect(() => { if (active) { startTransition(() => { setDelayedActive(true); }); } else { if (delayedActiveTimerRef.current) { clearTimeout(delayedActiveTimerRef.current); } delayedActiveTimerRef.current = setTimeout(() => { setDelayedActive(false); if (delayedActiveTimerRef.current) { clearTimeout(delayedActiveTimerRef.current); } }, duration); } return () => { if (delayedActiveTimerRef.current) { clearTimeout(delayedActiveTimerRef.current); } }; }, [active]); return {children}; } const MemoizedActivty = memo(_MemoizedActivty, (prevProps, nextProps) => { return prevProps.active === nextProps.active && prevProps.duration === nextProps.duration; }); MemoizedActivty.displayName = "MemoizedActivty"; export default MemoizedActivty; ================================================ FILE: packages/core/src/event/index.ts ================================================ import mitt, { Emitter } from "mitt"; type Events = { destroy: string[]; refresh: string; destroyAll: void; destroyOther: string; }; const eventBus: Emitter = mitt(); export default eventBus; ================================================ FILE: packages/core/src/hooks/onDestory.ts ================================================ import eventBus from "../event"; function useOnDestroy(cb: () => any, _key: string) { eventBus.on("destroy", cacheKeys => { if (cacheKeys.includes(_key)) { cb(); } }); eventBus.on("destroyAll", () => { cb(); }); eventBus.on("destroyOther", cacheKey => { if (cacheKey !== _key) { cb(); } }); eventBus.on("refresh", cacheKey => { if (cacheKey === _key) { cb(); } }); } export default useOnDestroy; ================================================ FILE: packages/core/src/hooks/useEffectOnActive.ts ================================================ import { DependencyList, useEffect } from "react"; import useOnActive from "./useOnActive"; const useEffectOnActive = (cb: () => any, deps: DependencyList, skipMount = false): void => { useOnActive(cb, deps, skipMount, useEffect); }; export default useEffectOnActive; ================================================ FILE: packages/core/src/hooks/useEffectOnCreate.ts ================================================ import { useEffect } from "react"; import useOnCreate from "./useOnCreate"; const useEffectOnCreate = (cb: () => any): void => { useOnCreate(cb, useEffect); }; export default useEffectOnCreate; ================================================ FILE: packages/core/src/hooks/useKeepAliveContext.ts ================================================ import { useContext } from "react"; import { CacheComponentContext } from "../components/CacheContext"; const useKeepAliveContext = () => { return useContext(CacheComponentContext); }; export default useKeepAliveContext; ================================================ FILE: packages/core/src/hooks/useLayoutEffectOnActive.ts ================================================ import { DependencyList, useLayoutEffect } from "react"; import useOnActive from "./useOnActive"; const useLayoutEffectOnActive = (cb: () => any, deps: DependencyList, skipMount = false): void => { useOnActive(cb, deps, skipMount, useLayoutEffect); }; export default useLayoutEffectOnActive; ================================================ FILE: packages/core/src/hooks/useLayoutEffectOnCreate.ts ================================================ import { useLayoutEffect } from "react"; import useOnCreate from "./useOnCreate"; const useLayoutEffectOnCreate = (cb: () => any): void => { useOnCreate(cb, useLayoutEffect); }; export default useLayoutEffectOnCreate; ================================================ FILE: packages/core/src/hooks/useOnActive.ts ================================================ import { DependencyList, useEffect, useLayoutEffect, useRef } from "react"; import useKeepAliveContext from "./useKeepAliveContext"; import { isFn } from "../utils"; function useOnActive(cb: () => any, deps: DependencyList, skipMount = false, effect: typeof useEffect | typeof useLayoutEffect) { const { active } = useKeepAliveContext(); const isMount = useRef(false); effect(() => { if (!active) return; if (skipMount && !isMount.current) { isMount.current = true; return; } const destroyCb = cb(); return () => { if (isFn(destroyCb)) { destroyCb(); } }; }, [active, ...deps]); } export default useOnActive; ================================================ FILE: packages/core/src/hooks/useOnCreate.ts ================================================ import { useEffect, useLayoutEffect, useRef } from "react"; import { isFn } from "../utils"; import useKeepAliveContext from "./useKeepAliveContext"; import useOnDestroy from "./onDestory"; function useOnCreate(cb: () => any, effect: typeof useEffect | typeof useLayoutEffect) { const isMount = useRef(false); const destroyedRef = useRef(false); const { _cacheKey } = useKeepAliveContext(); effect(() => { let destroyCb: any; if (isMount.current === false) { isMount.current = true; destroyCb = cb(); } useOnDestroy(() => { if (isFn(destroyCb) && !destroyedRef.current) { destroyedRef.current = true; destroyCb(); } }, _cacheKey); }, []); } export default useOnCreate; ================================================ FILE: packages/core/src/index.ts ================================================ import KeepAlive, { KeepAliveProps, KeepAliveRef, useKeepAliveRef } from "./components/KeepAlive"; import useEffectOnActive from "./hooks/useEffectOnActive"; import useKeepAliveContext from "./hooks/useKeepAliveContext"; import useLayoutEffectOnActive from "./hooks/useLayoutEffectOnActive"; import useEffectOnCreate from "./hooks/useEffectOnCreate"; import useLayoutEffectOnCreate from "./hooks/useLayoutEffectOnCreate"; /** * @deprecated since version 3.0.2. Use `useKeepAliveRef` instead. */ const useKeepaliveRef = useKeepAliveRef; export { KeepAlive, useKeepAliveRef, useKeepaliveRef, useEffectOnActive, useLayoutEffectOnActive, useKeepAliveContext, useEffectOnCreate, useLayoutEffectOnCreate, }; export type { KeepAliveRef, KeepAliveProps }; ================================================ FILE: packages/core/src/utils/index.tsx ================================================ export function isNil(value: any): value is null | undefined { return value === null || value === undefined; } export function isRegExp(value: any): value is RegExp { return Object.prototype.toString.call(value) === "[object RegExp]"; } export function isArr(value: any): value is Array { return Array.isArray(value); } export function isFn(value: any): value is Function { return typeof value === "function"; } export function domAttrSet(dom: HTMLDivElement) { return { set: (key: string, value: string) => { dom.setAttribute(key, value); return domAttrSet(dom); }, }; } export function delayAsync(milliseconds: number = 100): Promise { let _timeID: null | number | NodeJS.Timeout; return new Promise((resolve, _reject) => { _timeID = setTimeout(() => { resolve(); if (!isNil(_timeID)) { clearTimeout(_timeID); } }, milliseconds); }); } export function isInclude(include: Array | string | RegExp | undefined, val: string) { const includes = isArr(include) ? include : isNil(include) ? [] : [include]; return includes.some(include => { if (isRegExp(include)) { return include.test(val); } else { return val === include; } }); } export function macroTask(fn: () => void) { setTimeout(fn, 0); } ================================================ FILE: packages/core/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "rootDir": "src", "outDir": "dist" }, "include": ["src"] } ================================================ FILE: packages/core/tsup.config.ts ================================================ import { defineConfig } from "tsup"; export default defineConfig({ entry: ["src/index.ts"], format: ["cjs", "esm"], outExtension({ format }) { return { js: format === "cjs" ? ".cjs" : ".mjs", }; }, dts: true, clean: true, external: ["react", "react-dom", "react/jsx-runtime"], minify: true, treeshake: true, }); ================================================ FILE: packages/router/CHANGELOG.md ================================================ # keepalive-for-react-router ## 5.0.7 ### Patch Changes - change enableActivity to default false - Updated dependencies - keepalive-for-react@5.0.7 ## 5.0.5 ### Patch Changes - fix import error ## 5.0.4 ### Patch Changes - bug fix - bux fix - bug fixes - bug fixes - Bug fixes - bug fixes - bug fixes - bug fixes - Updated dependencies - Updated dependencies - Updated dependencies - Updated dependencies - Updated dependencies - Updated dependencies - Updated dependencies - Updated dependencies - Updated dependencies - keepalive-for-react@5.0.4 ## 5.0.4-beta.8 ### Patch Changes - bug fixes - Updated dependencies - keepalive-for-react@5.0.4-beta.8 ## 5.0.4-beta.7 ### Patch Changes - bug fixes - Updated dependencies - keepalive-for-react@5.0.4-beta.7 ## 5.0.4-beta.6 ### Patch Changes - bug fixes - Updated dependencies - keepalive-for-react@5.0.4-beta.6 ## 5.0.4-beta.5 ### Patch Changes - bug fixes - Updated dependencies - keepalive-for-react@5.0.4-beta.5 ## 5.0.4-beta.4 ### Patch Changes - bug fixes - Updated dependencies - keepalive-for-react@5.0.4-beta.4 ## 5.0.4-beta.2 ### Patch Changes - bug fixes ## 5.0.4-beta.1 ### Patch Changes - bug fixes - Updated dependencies - keepalive-for-react@5.0.4-beta.1 ## 5.0.4-beta.0 ### Patch Changes - Bug fixes - Updated dependencies - keepalive-for-react@5.0.4-beta.0 ================================================ FILE: packages/router/README.md ================================================ # KeepAlive for React Router ## Installation ```bash npm install keepalive-for-react keepalive-for-react-router ``` ### v6+ ```bash npm install react-router-dom keepalive-for-react keepalive-for-react-router@1.x.x ``` ### v7+ ```bash npm install react-router keepalive-for-react keepalive-for-react-router@2.x.x ``` ## Usage ```tsx // v6+ keepalive-for-react-router@1.x.x // v7+ keepalive-for-react-router@2.x.x import KeepAliveRouteOutlet from "keepalive-for-react-router"; function Layout() { return (
); } ``` ================================================ FILE: packages/router/package.json ================================================ { "name": "keepalive-for-react-router", "version": "5.0.7", "description": "React Router integration for keepalive-for-react", "homepage": "https://github.com/finedaybreak/keepalive-for-react", "repository": { "type": "git", "url": "git+https://github.com/finedaybreak/keepalive-for-react.git" }, "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.mjs", "require": "./dist/index.cjs" } }, "main": "./dist/index.cjs", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", "files": [ "dist" ], "type": "module", "scripts": { "build": "tsup", "clean": "rm -rf dist" }, "keywords": [ "keepalive", "keep-alive", "react keepalive", "keepalive for react", "keepalive-for-react", "keepalive-for-react-router" ], "author": "wongyichen", "license": "MIT", "peerDependencies": { "keepalive-for-react": "^5.0.7", "react": ">=16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": ">=16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-router": ">=6.0.0" }, "devDependencies": { "keepalive-for-react": "^5.0.7", "react-router": "^7.10.0" } } ================================================ FILE: packages/router/src/components/KeepAliveRouteOutlet/index.tsx ================================================ import { ComponentType, Fragment, ReactNode, useMemo } from "react"; import { useLocation, useOutlet } from "react-router"; import { KeepAlive, type KeepAliveProps } from "keepalive-for-react"; export interface KeepAliveRouteOutletProps extends Omit { wrapperComponent?: ComponentType<{ children: ReactNode }>; activeCacheKey?: string; } function KeepAliveRouteOutlet(props: KeepAliveRouteOutletProps) { const { wrapperComponent, activeCacheKey: propsActiveCacheKey, ...rest } = props; const location = useLocation(); const outlet = useOutlet(); const WrapperComponent = wrapperComponent || Fragment; const activeCacheKey = useMemo(() => { return propsActiveCacheKey || location.pathname + location.search; }, [location.pathname, location.search, propsActiveCacheKey]); return ( {outlet} ); } export default KeepAliveRouteOutlet; ================================================ FILE: packages/router/src/index.ts ================================================ import KeepAliveRouteOutlet from "./components/KeepAliveRouteOutlet"; export default KeepAliveRouteOutlet; ================================================ FILE: packages/router/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "rootDir": "src", "outDir": "dist" }, "include": ["src"] } ================================================ FILE: packages/router/tsup.config.ts ================================================ import { defineConfig } from "tsup"; export default defineConfig({ entry: ["src/index.ts"], format: ["cjs", "esm"], outExtension({ format }) { return { js: format === "cjs" ? ".cjs" : ".mjs", }; }, dts: true, clean: true, external: ["react", "react-dom", "react-router", "react/jsx-runtime", "keepalive-for-react"], minify: true, treeshake: true, }); ================================================ FILE: pnpm-workspace.yaml ================================================ packages: - "packages/*" ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "ES2020", "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "bundler", "jsx": "react-jsx", "strict": true, "skipLibCheck": true, "isolatedModules": true, "resolveJsonModule": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "declaration": true, "declarationMap": true }, "exclude": ["node_modules", "**/dist"] }