├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md ├── .gitignore ├── .husky └── pre-commit ├── .npmrc ├── .prettierrc ├── LICENSE ├── README.md ├── README.zh_CN.md ├── examples ├── react-router-dom-simple-starter │ ├── .gitignore │ ├── README.md │ ├── eslint.config.js │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.tsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── 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 │ ├── README.md │ ├── eslint.config.js │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── public │ └── vite.svg │ ├── src │ ├── App.tsx │ ├── assets │ │ └── react.svg │ ├── index.css │ ├── main.tsx │ └── vite-env.d.ts │ ├── tailwind.config.js │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── package.json ├── packages ├── core │ ├── README.md │ ├── README.zh_CN.md │ ├── package.json │ ├── react-keepalive.png │ ├── rollup.config.js │ ├── src │ │ ├── compat │ │ │ └── safeStartTransition.ts │ │ ├── components │ │ │ ├── CacheComponent │ │ │ │ └── index.tsx │ │ │ ├── CacheComponentProvider │ │ │ │ └── index.tsx │ │ │ ├── CacheContext │ │ │ │ └── index.tsx │ │ │ └── KeepAlive │ │ │ │ └── index.tsx │ │ ├── hooks │ │ │ ├── useEffectOnActive.ts │ │ │ ├── useKeepAliveContext.ts │ │ │ ├── useLayoutEffectOnActive.ts │ │ │ └── useOnActive.ts │ │ ├── index.ts │ │ └── utils │ │ │ └── index.tsx │ ├── tsconfig.json │ ├── tsconfig.types.json │ └── tsconfig.types.tsbuildinfo └── router │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ ├── components │ │ └── KeepAliveRouteOutlet │ │ │ └── index.tsx │ └── index.ts │ ├── tsconfig.json │ ├── tsconfig.types.json │ └── tsconfig.types.tsbuildinfo ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── publish.js ├── react-keepalive.png └── tsconfig.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Editor directories and files 4 | .vscode/* 5 | !.vscode/extensions.json 6 | .idea 7 | .DS_Store 8 | *.suo 9 | *.ntvs* 10 | *.njsproj 11 | *.sln 12 | *.sw? 13 | 14 | dist 15 | 16 | 17 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm exec lint-staged 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "singleQuote": false, 6 | "jsxSingleQuote": false, 7 | "semi": true, 8 | "trailingComma": "all", 9 | "bracketSpacing": true, 10 | "arrowParens": "avoid" 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Rychen Wong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | keepalive-for-react logo 3 |

4 | 5 |
6 |

7 | KeepAlive for React 8 |

9 |
10 | 11 |

A React KeepAlive component like keep-alive in vue

12 | 13 | [中文](./README.zh_CN.md) | English 14 | 15 | [![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]
16 | 17 | ## Packages 18 | 19 | | Package | Version | Description | 20 | | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | 21 | | [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 | 22 | | [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 | 23 | 24 | ## Features 25 | 26 | - Support react-router-dom v6+ or react-router v7+ 27 | - Support React v16+ ~ v18+ 28 | - Support Suspense and Lazy import 29 | - Support ErrorBoundary 30 | - Support Custom Container 31 | - Support Switching Animation Transition with className `active` and `inactive` 32 | - Simply implement, without any extra dependencies and hacking ways 33 | - Only 6KB minified size 34 | 35 | ## Attention 36 | 37 | - DO NOT use , it CANNOT work with keepalive-for-react in development mode. because it can lead to 38 | some unexpected behavior. 39 | 40 | - In Router only support react-router-dom v6+ 41 | 42 | ## Install 43 | 44 | ```bash 45 | npm install keepalive-for-react 46 | ``` 47 | 48 | ```bash 49 | yarn add keepalive-for-react 50 | ``` 51 | 52 | ```bash 53 | pnpm add keepalive-for-react 54 | ``` 55 | 56 | ## Usage 57 | 58 | ### in react-router-dom v6+ or react-router v7+ 59 | 60 | 1. install react-router-dom v6+ or react-router v7+ 61 | 62 | ```bash 63 | # v6+ 64 | npm install react-router-dom keepalive-for-react keepalive-for-react-router@1.x.x 65 | # v7+ 66 | npm install react-router keepalive-for-react keepalive-for-react-router@2.x.x 67 | ``` 68 | 69 | 2. use KeepAlive in your project 70 | 71 | ```tsx 72 | // v6+ keepalive-for-react-router@1.x.x 73 | // v7+ keepalive-for-react-router@2.x.x 74 | import KeepAliveRouteOutlet from "keepalive-for-react-router"; 75 | 76 | function Layout() { 77 | return ( 78 |
79 | 80 |
81 | ); 82 | } 83 | ``` 84 | 85 | or 86 | 87 | ```tsx 88 | import { useMemo } from "react"; 89 | // v6+ 90 | import { useLocation, useOutlet } from "react-router-dom"; 91 | // v7 92 | // import { useLocation, useOutlet } from "react-router"; 93 | import { KeepAlive, useKeepAliveRef } from "keepalive-for-react"; 94 | 95 | function Layout() { 96 | const location = useLocation(); 97 | const aliveRef = useKeepAliveRef(); 98 | 99 | const outlet = useOutlet(); 100 | 101 | // determine which route component to is active 102 | const currentCacheKey = useMemo(() => { 103 | return location.pathname + location.search; 104 | }, [location.pathname, location.search]); 105 | 106 | return ( 107 |
108 | 109 | 110 | }> 111 | {outlet} 112 | 113 | 114 | 115 |
116 | ); 117 | } 118 | ``` 119 | 120 | details see [examples/react-router-dom-simple-starter](./examples/react-router-dom-simple-starter) 121 | 122 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/irychen/keepalive-for-react/tree/main/examples/react-router-dom-simple-starter) 123 | 124 | ### in simple tabs 125 | 126 | ```bash 127 | npm install keepalive-for-react 128 | ``` 129 | 130 | ```tsx 131 | const tabs = [ 132 | { 133 | key: "tab1", 134 | label: "Tab 1", 135 | component: Tab1, 136 | }, 137 | { 138 | key: "tab2", 139 | label: "Tab 2", 140 | component: Tab2, 141 | }, 142 | { 143 | key: "tab3", 144 | label: "Tab 3", 145 | component: Tab3, 146 | }, 147 | ]; 148 | 149 | function App() { 150 | const [currentTab, setCurrentTab] = useState("tab1"); 151 | 152 | const tab = useMemo(() => { 153 | return tabs.find(tab => tab.key === currentTab); 154 | }, [currentTab]); 155 | 156 | return ( 157 |
158 | {/* ... */} 159 | 160 | {tab && } 161 | 162 |
163 | ); 164 | } 165 | ``` 166 | 167 | details see [examples/simple-tabs-starter](./examples/simple-tabs-starter) 168 | 169 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/irychen/keepalive-for-react/tree/main/examples/simple-tabs-starter) 170 | 171 | ## KeepAlive Props 172 | 173 | type definition 174 | 175 | ```tsx 176 | interface KeepAliveProps { 177 | // determine which component to is active 178 | activeCacheKey: string; 179 | children?: KeepAliveChildren; 180 | /** 181 | * max cache count default 10 182 | */ 183 | max?: number; 184 | exclude?: Array | string | RegExp; 185 | include?: Array | string | RegExp; 186 | onBeforeActive?: (activeCacheKey: string) => void; 187 | customContainerRef?: RefObject; 188 | cacheNodeClassName?: string; 189 | containerClassName?: string; 190 | errorElement?: ComponentType<{ 191 | children: ReactNode; 192 | }>; 193 | /** 194 | * transition default false 195 | */ 196 | transition?: boolean; 197 | /** 198 | * use view transition to animate the component when switching tabs 199 | * @see https://developer.chrome.com/docs/web-platform/view-transitions/ 200 | */ 201 | viewTransition?: boolean; 202 | /** 203 | * transition duration default 200 204 | */ 205 | duration?: number; 206 | aliveRef?: RefObject; 207 | /** 208 | * max alive time for cache node (second) 209 | * @default 0 (no limit) 210 | */ 211 | maxAliveTime?: number | MaxAliveConfig[]; 212 | } 213 | 214 | interface MaxAliveConfig { 215 | match: string | RegExp; 216 | expire: number; 217 | } 218 | ``` 219 | 220 | ## Hooks 221 | 222 | ### useEffectOnActive 223 | 224 | ```tsx 225 | useEffectOnActive(() => { 226 | console.log("active"); 227 | }, []); 228 | ``` 229 | 230 | ### useLayoutEffectOnActive 231 | 232 | ```tsx 233 | useLayoutEffectOnActive( 234 | () => { 235 | console.log("active"); 236 | }, 237 | [], 238 | false, 239 | ); 240 | // the third parameter is optional, default is false, 241 | // if true, which means the callback will be skipped when the useLayoutEffect is triggered in first render 242 | ``` 243 | 244 | ### useKeepAliveContext 245 | 246 | type definition 247 | 248 | ```ts 249 | interface KeepAliveContext { 250 | /** 251 | * whether the component is active 252 | */ 253 | active: boolean; 254 | /** 255 | * refresh the component 256 | * @param {string} [cacheKey] - The cache key of the component. If not provided, the current cached component will be refreshed. 257 | */ 258 | refresh: (cacheKey?: string) => void; 259 | /** 260 | * destroy the component 261 | * @param {string} [cacheKey] - the cache key of the component, if not provided, current active cached component will be destroyed 262 | */ 263 | destroy: (cacheKey?: string | string[]) => Promise; 264 | /** 265 | * destroy all components 266 | */ 267 | destroyAll: () => Promise; 268 | /** 269 | * destroy other components except the provided cacheKey 270 | * @param {string} [cacheKey] - The cache key of the component. If not provided, destroy all components except the current active cached component. 271 | */ 272 | destroyOther: (cacheKey?: string) => Promise; 273 | /** 274 | * get the cache nodes 275 | */ 276 | getCacheNodes: () => Array; 277 | } 278 | ``` 279 | 280 | ```tsx 281 | const { active, refresh, destroy, getCacheNodes } = useKeepAliveContext(); 282 | // active is a boolean, true is active, false is inactive 283 | // refresh is a function, you can call it to refresh the component 284 | // destroy is a function, you can call it to destroy the component 285 | // ... 286 | // getCacheNodes is a function, you can call it to get the cache nodes 287 | ``` 288 | 289 | ### useKeepAliveRef 290 | 291 | type definition 292 | 293 | ```ts 294 | interface KeepAliveRef { 295 | refresh: (cacheKey?: string) => void; 296 | destroy: (cacheKey?: string | string[]) => Promise; 297 | destroyAll: () => Promise; 298 | destroyOther: (cacheKey?: string) => Promise; 299 | getCacheNodes: () => Array; 300 | } 301 | ``` 302 | 303 | ```tsx 304 | function App() { 305 | const aliveRef = useKeepAliveRef(); 306 | // aliveRef.current is a KeepAliveRef object 307 | 308 | // you can call refresh and destroy on aliveRef.current 309 | aliveRef.current?.refresh(); 310 | // it is not necessary to call destroy manually, KeepAlive will handle it automatically 311 | aliveRef.current?.destroy(); 312 | 313 | return {/* ... */}; 314 | } 315 | // or 316 | function AppRouter() { 317 | const aliveRef = useKeepAliveRef(); 318 | // aliveRef.current is a KeepAliveRef object 319 | 320 | // you can call refresh and destroy on aliveRef.current 321 | aliveRef.current?.refresh(); 322 | aliveRef.current?.destroy(); 323 | return ; 324 | } 325 | ``` 326 | 327 | ## Development 328 | 329 | install dependencies 330 | 331 | ```bash 332 | pnpm install 333 | ``` 334 | 335 | build package 336 | 337 | ```bash 338 | pnpm build 339 | ``` 340 | 341 | link package to global 342 | 343 | ```bash 344 | pnpm link --global 345 | ``` 346 | 347 | test in demo project 348 | 349 | ```bash 350 | cd demo 351 | pnpm link --global keepalive-for-react 352 | ``` 353 | [discord-link]: https://discord.gg/ycf896w7eA 354 | [discord-shield]: https://img.shields.io/discord/1232158668913381467?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square 355 | [discord-shield-badge]: https://img.shields.io/discord/1232158668913381467?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=for-the-badge 356 | -------------------------------------------------------------------------------- /README.zh_CN.md: -------------------------------------------------------------------------------- 1 |

2 | keepalive-for-react logo 3 |

4 | 5 |
6 |

7 | React KeepAlive 组件 8 |

9 |
10 | 11 |

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

12 | 13 | [English](./README.md) | 中文 14 | 15 | [![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) 16 | 17 | ## 包信息 18 | 19 | | 包名 | 版本 | 描述 | 20 | | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | 21 | | [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功能 | 22 | | [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集成 | 23 | 24 | ## 特性 25 | 26 | - 支持react-router-dom v6+ 或 react-router v7+ 27 | - 支持React v16+ ~ v18+ 28 | - 支持Suspense和懒加载导入 29 | - 支持错误边界 30 | - 支持自定义容器 31 | - 支持使用className `active`和`inactive`进行切换动画过渡 32 | - 简单实现,无需任何额外依赖和hack方式 33 | - 压缩后仅6KB大小 34 | 35 | ## 注意事项 36 | 37 | - 请勿使用 ,它在开发模式下无法与keepalive-for-react一起工作。因为它可能会导致一些意外行为。 38 | 39 | - 在路由中仅支持react-router-dom v6+ 40 | 41 | ## 安装 42 | 43 | ```bash 44 | npm install keepalive-for-react 45 | ``` 46 | 47 | ```bash 48 | yarn add keepalive-for-react 49 | ``` 50 | 51 | ```bash 52 | pnpm add keepalive-for-react 53 | ``` 54 | 55 | ## 使用 56 | 57 | ### 配合react-router-dom v6+ 或 react-router v7+使用 58 | 59 | 1. 安装react-router-dom v6+ 或 react-router v7+ 60 | 61 | ```bash 62 | # v6+ 63 | npm install react-router-dom keepalive-for-react keepalive-for-react-router@1.x.x 64 | # v7+ 65 | npm install react-router keepalive-for-react keepalive-for-react-router@2.x.x 66 | ``` 67 | 68 | 2. 在项目中使用KeepAlive 69 | 70 | ```tsx 71 | // v6+ keepalive-for-react-router@1.x.x 72 | // v7+ keepalive-for-react-router@2.x.x 73 | import KeepAliveRouteOutlet from "keepalive-for-react-router"; 74 | 75 | function Layout() { 76 | return ( 77 |
78 | 79 |
80 | ); 81 | } 82 | ``` 83 | 84 | 或者 85 | 86 | ```tsx 87 | import { useMemo } from "react"; 88 | import { useLocation } from "react-router-dom"; 89 | import { KeepAlive, useKeepAliveRef } from "keepalive-for-react"; 90 | 91 | function Layout() { 92 | const location = useLocation(); 93 | const aliveRef = useKeepAliveRef(); 94 | 95 | const outlet = useOutlet(); 96 | 97 | // 确定哪个路由组件处于活动状态 98 | const currentCacheKey = useMemo(() => { 99 | return location.pathname + location.search; 100 | }, [location.pathname, location.search]); 101 | 102 | return ( 103 |
104 | 105 | 106 | }> 107 | {outlet} 108 | 109 | 110 | 111 |
112 | ); 113 | } 114 | ``` 115 | 116 | 详情请参见 [examples/react-router-dom-simple-starter](./examples/react-router-dom-simple-starter) 117 | 118 | [![在StackBlitz中打开](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/irychen/keepalive-for-react/tree/main/examples/react-router-dom-simple-starter) 119 | 120 | ### 在简单标签页中 121 | 122 | ```bash 123 | npm install keepalive-for-react 124 | ``` 125 | 126 | ```tsx 127 | const tabs = [ 128 | { 129 | key: "tab1", 130 | label: "标签1", 131 | component: Tab1, 132 | }, 133 | { 134 | key: "tab2", 135 | label: "标签2", 136 | component: Tab2, 137 | }, 138 | { 139 | key: "tab3", 140 | label: "标签3", 141 | component: Tab3, 142 | }, 143 | ]; 144 | 145 | function App() { 146 | const [currentTab, setCurrentTab] = useState("tab1"); 147 | 148 | const tab = useMemo(() => { 149 | return tabs.find(tab => tab.key === currentTab); 150 | }, [currentTab]); 151 | 152 | return ( 153 |
154 | {/* ... */} 155 | 156 | {tab && } 157 | 158 |
159 | ); 160 | } 161 | ``` 162 | 163 | 详情请参见 [examples/simple-tabs-starter](./examples/simple-tabs-starter) 164 | 165 | [![在StackBlitz中打开](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/irychen/keepalive-for-react/tree/main/examples/simple-tabs-starter) 166 | 167 | ## KeepAlive 属性 168 | 169 | 类型定义 170 | 171 | ```tsx 172 | interface KeepAliveProps { 173 | // 确定哪个组件处于活动状态 174 | activeCacheKey: string; 175 | children?: KeepAliveChildren; 176 | /** 177 | * 最大缓存数量 默认10 178 | */ 179 | max?: number; 180 | exclude?: Array | string | RegExp; 181 | include?: Array | string | RegExp; 182 | onBeforeActive?: (activeCacheKey: string) => void; 183 | customContainerRef?: RefObject; 184 | cacheNodeClassName?: string; 185 | containerClassName?: string; 186 | errorElement?: ComponentType<{ 187 | children: ReactNode; 188 | }>; 189 | /** 190 | * 过渡效果 默认false 191 | */ 192 | transition?: boolean; 193 | 194 | /** 195 | * 使用view transition来过渡组件 默认false 196 | * @see https://developer.chrome.com/docs/web-platform/view-transitions/ 197 | */ 198 | viewTransition?: boolean; 199 | 200 | /** 201 | * 过渡时间 默认200ms 202 | */ 203 | duration?: number; 204 | aliveRef?: RefObject; 205 | /** 206 | * 缓存节点最大存活时间 (秒) 207 | * @default 0 (无限制) 208 | */ 209 | maxAliveTime?: number | MaxAliveConfig[]; 210 | } 211 | 212 | interface MaxAliveConfig { 213 | match: string | RegExp; 214 | expire: number; 215 | } 216 | ``` 217 | 218 | ## Hooks 219 | 220 | ### useEffectOnActive 221 | 222 | ```tsx 223 | useEffectOnActive(() => { 224 | console.log("active"); 225 | }, []); 226 | ``` 227 | 228 | ### useLayoutEffectOnActive 229 | 230 | ```tsx 231 | useLayoutEffectOnActive( 232 | () => { 233 | console.log("active"); 234 | }, 235 | [], 236 | false, 237 | ); 238 | // 第三个参数是可选的,默认为false, 239 | // 如果为true,表示在首次渲染时触发useLayoutEffect时会跳过回调 240 | ``` 241 | 242 | ### useKeepAliveContext 243 | 244 | 类型定义 245 | 246 | ```ts 247 | interface KeepAliveContext { 248 | /** 249 | * 组件是否处于活动状态 250 | */ 251 | active: boolean; 252 | /** 253 | * 刷新组件 254 | * @param {string} [cacheKey] - 组件的缓存键。如果未提供,将刷新当前缓存的组件。 255 | */ 256 | refresh: (cacheKey?: string) => void; 257 | /** 258 | * 销毁组件 259 | * @param {string} [cacheKey] - 组件的缓存键,如果未提供,将销毁当前活动的缓存组件。 260 | */ 261 | destroy: (cacheKey?: string | string[]) => Promise; 262 | /** 263 | * 销毁所有组件 264 | */ 265 | destroyAll: () => Promise; 266 | /** 267 | * 销毁除提供的cacheKey外的其他组件 268 | * @param {string} [cacheKey] - 组件的缓存键。如果未提供,将销毁除当前活动缓存组件外的所有组件。 269 | */ 270 | destroyOther: (cacheKey?: string) => Promise; 271 | /** 272 | * 获取缓存节点 273 | */ 274 | getCacheNodes: () => Array; 275 | } 276 | ``` 277 | 278 | ```tsx 279 | const { active, refresh, destroy, getCacheNodes } = useKeepAliveContext(); 280 | // active 是一个布尔值,true表示活动,false表示非活动 281 | // refresh 是一个函数,你可以调用它来刷新组件 282 | // destroy 是一个函数,你可以调用它来销毁组件 283 | // ... 284 | // getCacheNodes 是一个函数,你可以调用它来获取缓存节点 285 | ``` 286 | 287 | ### useKeepAliveRef 288 | 289 | 类型定义 290 | 291 | ```ts 292 | interface KeepAliveRef { 293 | refresh: (cacheKey?: string) => void; 294 | destroy: (cacheKey?: string | string[]) => Promise; 295 | destroyAll: () => Promise; 296 | destroyOther: (cacheKey?: string) => Promise; 297 | getCacheNodes: () => Array; 298 | } 299 | ``` 300 | 301 | ```tsx 302 | function App() { 303 | const aliveRef = useKeepAliveRef(); 304 | // aliveRef.current 是一个 KeepAliveRef 对象 305 | 306 | // 你可以在 aliveRef.current 上调用 refresh 和 destroy 307 | aliveRef.current?.refresh(); 308 | // 通常不需要手动调用 destroy,KeepAlive 会自动处理 309 | aliveRef.current?.destroy(); 310 | 311 | return {/* ... */}; 312 | } 313 | // 或者 314 | function AppRouter() { 315 | const aliveRef = useKeepAliveRef(); 316 | // aliveRef.current 是一个 KeepAliveRef 对象 317 | 318 | // 你可以在 aliveRef.current 上调用 refresh 和 destroy 319 | aliveRef.current?.refresh(); 320 | aliveRef.current?.destroy(); 321 | return ; 322 | } 323 | ``` 324 | 325 | ## 开发 326 | 327 | 安装依赖 328 | 329 | ```bash 330 | pnpm install 331 | ``` 332 | 333 | 构建包 334 | 335 | ```bash 336 | pnpm build 337 | ``` 338 | 339 | 链接包到全局 340 | 341 | ```bash 342 | pnpm link --global 343 | ``` 344 | 345 | 在演示项目中测试 346 | 347 | ```bash 348 | cd demo 349 | pnpm link --global keepalive-for-react 350 | ``` 351 | -------------------------------------------------------------------------------- /examples/react-router-dom-simple-starter/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/react-router-dom-simple-starter/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@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 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default tseslint.config({ 18 | languageOptions: { 19 | // other options... 20 | parserOptions: { 21 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 22 | tsconfigRootDir: import.meta.dirname, 23 | }, 24 | }, 25 | }) 26 | ``` 27 | 28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` 29 | - Optionally add `...tseslint.configs.stylisticTypeChecked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: 31 | 32 | ```js 33 | // eslint.config.js 34 | import react from 'eslint-plugin-react' 35 | 36 | export default tseslint.config({ 37 | // Set the react version 38 | settings: { react: { version: '18.3' } }, 39 | plugins: { 40 | // Add the react plugin 41 | react, 42 | }, 43 | rules: { 44 | // other rules... 45 | // Enable its recommended rules 46 | ...react.configs.recommended.rules, 47 | ...react.configs['jsx-runtime'].rules, 48 | }, 49 | }) 50 | ``` 51 | -------------------------------------------------------------------------------- /examples/react-router-dom-simple-starter/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /examples/react-router-dom-simple-starter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/react-router-dom-simple-starter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-router-dom-simple-starter", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "keepalive-for-react": "^4.0.2", 14 | "keepalive-for-react-router": "^2.0.2", 15 | "react": "^19.1.0", 16 | "react-dom": "^19.1.0", 17 | "react-router": "^7.5.0" 18 | }, 19 | "devDependencies": { 20 | "@eslint/js": "^9.11.1", 21 | "@types/react": "^19.1.0", 22 | "@types/react-dom": "^19.1.1", 23 | "@vitejs/plugin-react": "^4.3.2", 24 | "autoprefixer": "^10.4.20", 25 | "eslint": "^9.11.1", 26 | "eslint-plugin-react-hooks": "^5.1.0-rc.0", 27 | "eslint-plugin-react-refresh": "^0.4.12", 28 | "globals": "^15.9.0", 29 | "postcss": "^8.4.47", 30 | "tailwindcss": "^3.4.14", 31 | "typescript": "^5.5.3", 32 | "typescript-eslint": "^8.7.0", 33 | "vite": "^6.2.5" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/react-router-dom-simple-starter/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /examples/react-router-dom-simple-starter/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/react-router-dom-simple-starter/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from "react"; 2 | import { RouterProvider } from "react-router"; 3 | import router from "./router"; 4 | 5 | function App() { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | 13 | export default App; 14 | -------------------------------------------------------------------------------- /examples/react-router-dom-simple-starter/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/react-router-dom-simple-starter/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* 6 | keepalive-for-react animation example 7 | transition should be set true to enable animation 8 | and duration should be set to the animation duration 9 | */ 10 | 11 | .cache-component.active { 12 | animation: fadeIn 0.3s ease-in-out, slideIn 0.3s ease-in-out; 13 | } 14 | 15 | .cache-component.inactive { 16 | animation: fadeOut 0.3s ease-in-out, slideOut 0.3s ease-in-out; 17 | } 18 | 19 | @keyframes fadeIn { 20 | from { 21 | opacity: 0; 22 | } 23 | to { 24 | opacity: 1; 25 | } 26 | } 27 | 28 | @keyframes fadeOut { 29 | from { 30 | opacity: 1; 31 | } 32 | to { 33 | opacity: 0; 34 | } 35 | } 36 | 37 | @keyframes slideIn { 38 | from { 39 | transform: translateX(-100%); 40 | } 41 | to { 42 | transform: translateX(0); 43 | } 44 | } 45 | 46 | @keyframes slideOut { 47 | from { 48 | transform: translateX(0); 49 | } 50 | to { 51 | transform: translateX(100%); 52 | } 53 | } -------------------------------------------------------------------------------- /examples/react-router-dom-simple-starter/src/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import { useKeepAliveRef } from "keepalive-for-react"; 2 | import KeepAliveRouteOutlet from "keepalive-for-react-router"; 3 | import { ReactNode, Suspense, useEffect, useMemo, useRef } from "react"; 4 | import { Link, useLocation } from "react-router"; 5 | 6 | function Layout() { 7 | const location = useLocation(); 8 | const activePath = location.pathname + location.search; 9 | const aliveRef = useKeepAliveRef(); 10 | return ( 11 |
12 |
13 | 14 | Home 15 | 16 | 17 | About 18 | 19 | 20 | Counter 21 | 22 | 23 | Counter2 24 | 25 | 26 | NestedA 27 | 28 | 29 | NestedB 30 | 31 |
32 |
33 | 34 | 41 | 42 |
43 |
44 | ); 45 | } 46 | 47 | // remember the scroll position of the page when switching routes 48 | function MemoScrollTopWrapper(props: { children?: ReactNode }) { 49 | const { children } = props; 50 | const domRef = useRef(null); 51 | const location = useLocation(); 52 | const scrollHistoryMap = useRef>(new Map()); 53 | 54 | const activeKey = useMemo(() => { 55 | return location.pathname + location.search; 56 | }, [location.pathname, location.search]); 57 | 58 | useEffect(() => { 59 | const divDom = domRef.current; 60 | if (!divDom) return; 61 | setTimeout(() => { 62 | divDom.scrollTo(0, scrollHistoryMap.current.get(activeKey) || 0); 63 | }, 300); // 300 milliseconds to wait for the animation transition ending 64 | const onScroll = (e: Event) => { 65 | const target = e.target as HTMLDivElement; 66 | if (!target) return; 67 | scrollHistoryMap.current.set(activeKey, target?.scrollTop || 0); 68 | }; 69 | divDom?.addEventListener("scroll", onScroll, { 70 | passive: true, 71 | }); 72 | return () => { 73 | divDom?.removeEventListener("scroll", onScroll); 74 | }; 75 | }, [activeKey]); 76 | 77 | return ( 78 |
85 | {children} 86 |
87 | ); 88 | } 89 | 90 | function CustomSuspense(props: { children: ReactNode }) { 91 | const { children } = props; 92 | return Loading...}>{children}; 93 | } 94 | 95 | export default Layout; 96 | -------------------------------------------------------------------------------- /examples/react-router-dom-simple-starter/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from "react-dom/client"; 2 | import App from "./App.tsx"; 3 | import "./index.css"; 4 | 5 | createRoot(document.getElementById("root")!).render(); 6 | -------------------------------------------------------------------------------- /examples/react-router-dom-simple-starter/src/pages/about/index.tsx: -------------------------------------------------------------------------------- 1 | function About() { 2 | return ( 3 |
4 |

About

5 |
6 | 7 |
8 |
9 | ); 10 | } 11 | 12 | export default About; 13 | -------------------------------------------------------------------------------- /examples/react-router-dom-simple-starter/src/pages/counter/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffectOnActive, useKeepAliveContext } from "keepalive-for-react"; 2 | import { useState } from "react"; 3 | 4 | function Counter() { 5 | const [count, setCount] = useState(0); 6 | const { refresh, active } = useKeepAliveContext(); 7 | 8 | useEffectOnActive(() => { 9 | console.log("Counter is active", count); 10 | }, [count]); 11 | 12 | return ( 13 |
14 |

Counter

15 |
16 | 17 | Active: {active ? "true" : "false"} 18 | 19 |
20 |
{count}
21 |
22 | 28 | 34 | 37 |
38 |
39 | ); 40 | } 41 | 42 | export default Counter; 43 | -------------------------------------------------------------------------------- /examples/react-router-dom-simple-starter/src/pages/home/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffectOnActive } from "keepalive-for-react"; 2 | import { useRef } from "react"; 3 | 4 | function Home() { 5 | const domRef = useRef(null); 6 | useEffectOnActive( 7 | () => { 8 | console.log("Home is active"); 9 | const dom = domRef.current; 10 | if (dom) { 11 | // if transition is true, the dom size will be 0, because the dom is not rendered yet 12 | console.log(`home dom size: height ${dom.clientHeight}px width ${dom.clientWidth}px`); 13 | setTimeout(() => { 14 | console.log(`home dom size: height ${dom.clientHeight}px width ${dom.clientWidth}px`); 15 | }, 300); 16 | } 17 | }, 18 | [], 19 | true, 20 | ); 21 | 22 | return ( 23 |
24 |

Home

25 |

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

28 |

Install

29 | 30 |
{`npm install keepalive-for-react react-router-dom`}
31 |
32 |
{"./src/layout/index.tsx"}
33 | 34 |
{``}
35 |
36 | 42 |
43 |
44 | ); 45 | } 46 | 47 | export default Home; 48 | -------------------------------------------------------------------------------- /examples/react-router-dom-simple-starter/src/pages/nested/index.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from "react-router"; 2 | 3 | function Nested() { 4 | return ( 5 |
6 |

Nested

7 | 8 |
9 | ); 10 | } 11 | 12 | export default Nested; 13 | -------------------------------------------------------------------------------- /examples/react-router-dom-simple-starter/src/pages/nested/nested-a/index.tsx: -------------------------------------------------------------------------------- 1 | function NestedA() { 2 | return ( 3 |
4 |
NestedA
5 |

This is a nested route. It will be cached.

6 | 7 |
8 | ); 9 | } 10 | 11 | export default NestedA; 12 | -------------------------------------------------------------------------------- /examples/react-router-dom-simple-starter/src/pages/nested/nested-b/index.tsx: -------------------------------------------------------------------------------- 1 | function NestedB() { 2 | return ( 3 |
4 |
NestedB
5 |

This is a nested route. It will be cached.

6 | 7 |
8 | ); 9 | } 10 | 11 | export default NestedB; 12 | -------------------------------------------------------------------------------- /examples/react-router-dom-simple-starter/src/pages/nocache-counter/index.tsx: -------------------------------------------------------------------------------- 1 | import { useKeepAliveContext } from "keepalive-for-react"; 2 | import { useState } from "react"; 3 | 4 | function NoCacheCounter() { 5 | const [count, setCount] = useState(0); 6 | const { refresh } = useKeepAliveContext(); 7 | 8 | return ( 9 |
10 |

Counter (No Cache)

11 |
{count}
12 |
13 | 19 | 25 | 28 |
29 |
30 | ); 31 | } 32 | 33 | export default NoCacheCounter; 34 | -------------------------------------------------------------------------------- /examples/react-router-dom-simple-starter/src/router/index.tsx: -------------------------------------------------------------------------------- 1 | import { createBrowserRouter } from "react-router"; 2 | import Layout from "../layout"; 3 | import { lazy } from "react"; 4 | import Nested from "../pages/nested"; 5 | import NestedA from "../pages/nested/nested-a"; 6 | import NestedB from "../pages/nested/nested-b"; 7 | // import Home from "../pages/home"; 8 | // import About from "../pages/about"; 9 | // import Counter from "../pages/counter"; 10 | // import NoCacheCounter from "../pages/nocache-counter"; 11 | 12 | // lazy load 13 | const Home = lazy(() => import("../pages/home")); 14 | const About = lazy(() => import("../pages/about")); 15 | const Counter = lazy(() => import("../pages/counter")); 16 | const NoCacheCounter = lazy(() => import("../pages/nocache-counter")); 17 | 18 | const router = createBrowserRouter([ 19 | { 20 | path: "/", 21 | element: , 22 | children: [ 23 | { 24 | path: "", 25 | element: , 26 | }, 27 | { 28 | path: "/about", 29 | element: , 30 | }, 31 | { 32 | path: "/counter", 33 | element: , 34 | }, 35 | { 36 | path: "/nocache-counter", 37 | element: , 38 | }, 39 | { 40 | path: "/nested", 41 | element: , 42 | children: [ 43 | { 44 | path: "nested-a", 45 | element: , 46 | }, 47 | { 48 | path: "nested-b", 49 | element: , 50 | }, 51 | ], 52 | }, 53 | ], 54 | }, 55 | ]); 56 | 57 | export default router; 58 | -------------------------------------------------------------------------------- /examples/react-router-dom-simple-starter/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/react-router-dom-simple-starter/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | 13 | -------------------------------------------------------------------------------- /examples/react-router-dom-simple-starter/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/react-router-dom-simple-starter/tsconfig.app.tsbuildinfo: -------------------------------------------------------------------------------- 1 | {"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/nocache-counter/index.tsx","./src/router/index.tsx"],"version":"5.6.3"} -------------------------------------------------------------------------------- /examples/react-router-dom-simple-starter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] 4 | } 5 | -------------------------------------------------------------------------------- /examples/react-router-dom-simple-starter/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["ES2023"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "isolatedModules": true, 12 | "moduleDetection": "force", 13 | "noEmit": true, 14 | 15 | /* Linting */ 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": ["vite.config.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /examples/react-router-dom-simple-starter/tsconfig.node.tsbuildinfo: -------------------------------------------------------------------------------- 1 | {"root":["./vite.config.ts"],"version":"5.6.3"} -------------------------------------------------------------------------------- /examples/react-router-dom-simple-starter/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | -------------------------------------------------------------------------------- /examples/simple-tabs-starter/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/simple-tabs-starter/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@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 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default tseslint.config({ 18 | languageOptions: { 19 | // other options... 20 | parserOptions: { 21 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 22 | tsconfigRootDir: import.meta.dirname, 23 | }, 24 | }, 25 | }) 26 | ``` 27 | 28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` 29 | - Optionally add `...tseslint.configs.stylisticTypeChecked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: 31 | 32 | ```js 33 | // eslint.config.js 34 | import react from 'eslint-plugin-react' 35 | 36 | export default tseslint.config({ 37 | // Set the react version 38 | settings: { react: { version: '18.3' } }, 39 | plugins: { 40 | // Add the react plugin 41 | react, 42 | }, 43 | rules: { 44 | // other rules... 45 | // Enable its recommended rules 46 | ...react.configs.recommended.rules, 47 | ...react.configs['jsx-runtime'].rules, 48 | }, 49 | }) 50 | ``` 51 | -------------------------------------------------------------------------------- /examples/simple-tabs-starter/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /examples/simple-tabs-starter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/simple-tabs-starter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-tabs-starter", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "keepalive-for-react": "^3.0.0", 14 | "react": "^18.3.1", 15 | "react-dom": "^18.3.1" 16 | }, 17 | "devDependencies": { 18 | "@eslint/js": "^9.11.1", 19 | "@types/react": "^18.3.10", 20 | "@types/react-dom": "^18.3.0", 21 | "@vitejs/plugin-react": "^4.3.2", 22 | "autoprefixer": "^10.4.20", 23 | "eslint": "^9.11.1", 24 | "eslint-plugin-react-hooks": "^5.1.0-rc.0", 25 | "eslint-plugin-react-refresh": "^0.4.12", 26 | "globals": "^15.9.0", 27 | "postcss": "^8.4.47", 28 | "tailwindcss": "^3.4.14", 29 | "typescript": "^5.5.3", 30 | "typescript-eslint": "^8.7.0", 31 | "vite": "^5.4.8" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/simple-tabs-starter/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /examples/simple-tabs-starter/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/simple-tabs-starter/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo, useState } from "react"; 2 | import { useEffectOnActive, useKeepAliveContext, KeepAlive } from "keepalive-for-react"; 3 | 4 | const tabs = [ 5 | { 6 | key: "tab1", 7 | label: "Tab 1", 8 | component: Tab1, 9 | }, 10 | { 11 | key: "tab2", 12 | label: "Tab 2", 13 | component: Tab2, 14 | }, 15 | { 16 | key: "tab3", 17 | label: "Tab 3", 18 | component: Tab3, 19 | }, 20 | ]; 21 | 22 | function App() { 23 | const [currentTab, setCurrentTab] = useState("tab1"); 24 | 25 | const tab = useMemo(() => { 26 | return tabs.find(tab => tab.key === currentTab); 27 | }, [currentTab]); 28 | 29 | const activeClass = "tab-item cursor-pointer text-blue-500"; 30 | const inactiveClass = "tab-item cursor-pointer"; 31 | 32 | return ( 33 |
34 |
35 | {tabs.map(tab => ( 36 |
setCurrentTab(tab.key)} 40 | > 41 | {tab.label} 42 |
43 | ))} 44 |
45 |
46 | 47 | {tab && } 48 | 49 |
50 |
51 | ); 52 | } 53 | 54 | function Tab1() { 55 | const [text, setText] = useState("Hello KeepAlive for React"); 56 | const { refresh } = useKeepAliveContext(); 57 | return ( 58 |
59 |
Tab1
60 |

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

64 | 71 | 72 |
73 | 76 | 79 |
80 | 86 |
87 | ); 88 | } 89 | 90 | function Tab2() { 91 | const [count, setCount] = useState(0); 92 | const { refresh, active } = useKeepAliveContext(); 93 | 94 | useEffectOnActive(() => { 95 | console.log("Tab2 Counter is active", count); 96 | }, [count]); 97 | return ( 98 |
99 |
Tab2
100 |
101 | 102 | Active: {active ? "true" : "false"} 103 | 104 |
105 |
{count}
106 |
107 | 113 | 119 | 122 |
123 |
124 | ); 125 | } 126 | 127 | function Tab3() { 128 | const [count, setCount] = useState(0); 129 | const { refresh, active } = useKeepAliveContext(); 130 | 131 | useEffectOnActive(() => { 132 | console.log("Tab3 Counter is active", count, active); 133 | }, [count]); 134 | return ( 135 |
136 |
Tab3 (no cache)
137 |
{count}
138 |
139 | 145 | 151 | 154 |
155 |
156 | ); 157 | } 158 | 159 | export default App; 160 | -------------------------------------------------------------------------------- /examples/simple-tabs-starter/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/simple-tabs-starter/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .cache-component.active { 6 | animation: fadeIn 0.2s ease-in-out; 7 | } 8 | 9 | .cache-component.inactive { 10 | animation: fadeOut 0.2s ease-in-out; 11 | } 12 | 13 | @keyframes fadeIn { 14 | from { 15 | opacity: 0; 16 | } 17 | to { 18 | opacity: 1; 19 | } 20 | } 21 | 22 | @keyframes fadeOut { 23 | from { 24 | opacity: 1; 25 | } 26 | to { 27 | opacity: 0; 28 | } 29 | } -------------------------------------------------------------------------------- /examples/simple-tabs-starter/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from "react-dom/client"; 2 | import App from "./App.tsx"; 3 | import "./index.css"; 4 | 5 | createRoot(document.getElementById("root")!).render(); 6 | -------------------------------------------------------------------------------- /examples/simple-tabs-starter/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/simple-tabs-starter/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | 13 | -------------------------------------------------------------------------------- /examples/simple-tabs-starter/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/simple-tabs-starter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] 4 | } 5 | -------------------------------------------------------------------------------- /examples/simple-tabs-starter/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["ES2023"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "isolatedModules": true, 12 | "moduleDetection": "force", 13 | "noEmit": true, 14 | 15 | /* Linting */ 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": ["vite.config.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /examples/simple-tabs-starter/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keepalive-for-react", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "A react component like in vue", 6 | "lint-staged": { 7 | "*": [ 8 | "prettier --write --cache --ignore-unknown" 9 | ], 10 | "packages/**/*.{js,ts,json,tsx}": [ 11 | "prettier --write" 12 | ] 13 | }, 14 | "type": "module", 15 | "scripts": { 16 | "format": "prettier --write \"**/*.{ts,tsx,json}\"", 17 | "test": "echo \"Error: no test specified\" && exit 1", 18 | "build": "pnpm -r build", 19 | "clean": "pnpm -r clean", 20 | "prepare": "husky", 21 | "publish:core": "cd packages/core && npm publish --access public", 22 | "publish:router": "cd packages/router && npm publish --access public", 23 | "publish:all": "pnpm build && pnpm publish:core && pnpm publish:router", 24 | "version:core": "cd packages/core && npm version", 25 | "version:router": "cd packages/router && npm version", 26 | "publish": "node publish.js" 27 | }, 28 | "author": "wongyichen", 29 | "license": "MIT", 30 | "devDependencies": { 31 | "@babel/core": "^7.23.0", 32 | "@babel/plugin-transform-runtime": "^7.22.15", 33 | "@babel/preset-env": "^7.22.20", 34 | "@babel/preset-react": "^7.22.15", 35 | "@babel/preset-typescript": "^7.23.0", 36 | "@babel/runtime": "^7.23.1", 37 | "@rollup/plugin-babel": "^6.0.4", 38 | "@rollup/plugin-commonjs": "^28.0.1", 39 | "@rollup/plugin-node-resolve": "^15.2.2", 40 | "@rollup/plugin-terser": "^0.4.4", 41 | "@rollup/plugin-typescript": "^11.1.5", 42 | "@types/node": "^20.8.2", 43 | "@types/react": "18.2.0", 44 | "@types/react-dom": "18.2.0", 45 | "husky": "^9.0.11", 46 | "lint-staged": "^15.2.2", 47 | "prettier": "^3.0.3", 48 | "rollup": "^4.0.2", 49 | "tslib": "^2.6.2", 50 | "typescript": "^5.2.2" 51 | }, 52 | "engines": { 53 | "node": ">=16", 54 | "npm": ">=8" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 |

2 | keepalive-for-react logo 3 |

4 | 5 |
6 |

7 | KeepAlive for React 8 |

9 |
10 | 11 |

A React KeepAlive component like keep-alive in vue

12 | 13 | [中文](./README.zh_CN.md) | English 14 | 15 | [![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) 16 | 17 | ## Packages 18 | 19 | | Package | Version | Description | 20 | | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | 21 | | [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 | 22 | | [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 | 23 | 24 | ## Features 25 | 26 | - Support react-router-dom v6+ or react-router v7+ 27 | - Support React v16+ ~ v18+ 28 | - Support Suspense and Lazy import 29 | - Support ErrorBoundary 30 | - Support Custom Container 31 | - Support Switching Animation Transition with className `active` and `inactive` 32 | - Simply implement, without any extra dependencies and hacking ways 33 | - Only 6KB minified size 34 | 35 | ## Attention 36 | 37 | - DO NOT use , it CANNOT work with keepalive-for-react in development mode. because it can lead to 38 | some unexpected behavior. 39 | 40 | - In Router only support react-router-dom v6+ 41 | 42 | ## Install 43 | 44 | ```bash 45 | npm install keepalive-for-react 46 | ``` 47 | 48 | ```bash 49 | yarn add keepalive-for-react 50 | ``` 51 | 52 | ```bash 53 | pnpm add keepalive-for-react 54 | ``` 55 | 56 | ## Usage 57 | 58 | ### in react-router-dom v6+ or react-router v7+ 59 | 60 | 1. install react-router-dom v6+ or react-router v7+ 61 | 62 | ```bash 63 | # v6+ 64 | npm install react-router-dom keepalive-for-react keepalive-for-react-router@1.x.x 65 | # v7+ 66 | npm install react-router keepalive-for-react keepalive-for-react-router@2.x.x 67 | ``` 68 | 69 | 2. use KeepAlive in your project 70 | 71 | ```tsx 72 | // v6+ keepalive-for-react-router@1.x.x 73 | // v7+ keepalive-for-react-router@2.x.x 74 | import KeepAliveRouteOutlet from "keepalive-for-react-router"; 75 | 76 | function Layout() { 77 | return ( 78 |
79 | 80 |
81 | ); 82 | } 83 | ``` 84 | 85 | or 86 | 87 | ```tsx 88 | import { useMemo } from "react"; 89 | // v6+ 90 | import { useLocation, useOutlet } from "react-router-dom"; 91 | // v7 92 | // import { useLocation, useOutlet } from "react-router"; 93 | import { KeepAlive, useKeepAliveRef } from "keepalive-for-react"; 94 | 95 | function Layout() { 96 | const location = useLocation(); 97 | const aliveRef = useKeepAliveRef(); 98 | 99 | const outlet = useOutlet(); 100 | 101 | // determine which route component to is active 102 | const currentCacheKey = useMemo(() => { 103 | return location.pathname + location.search; 104 | }, [location.pathname, location.search]); 105 | 106 | return ( 107 |
108 | 109 | 110 | }> 111 | {outlet} 112 | 113 | 114 | 115 |
116 | ); 117 | } 118 | ``` 119 | 120 | details see [examples/react-router-dom-simple-starter](./examples/react-router-dom-simple-starter) 121 | 122 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/irychen/keepalive-for-react/tree/main/examples/react-router-dom-simple-starter) 123 | 124 | ### in simple tabs 125 | 126 | ```bash 127 | npm install keepalive-for-react 128 | ``` 129 | 130 | ```tsx 131 | const tabs = [ 132 | { 133 | key: "tab1", 134 | label: "Tab 1", 135 | component: Tab1, 136 | }, 137 | { 138 | key: "tab2", 139 | label: "Tab 2", 140 | component: Tab2, 141 | }, 142 | { 143 | key: "tab3", 144 | label: "Tab 3", 145 | component: Tab3, 146 | }, 147 | ]; 148 | 149 | function App() { 150 | const [currentTab, setCurrentTab] = useState("tab1"); 151 | 152 | const tab = useMemo(() => { 153 | return tabs.find(tab => tab.key === currentTab); 154 | }, [currentTab]); 155 | 156 | return ( 157 |
158 | {/* ... */} 159 | 160 | {tab && } 161 | 162 |
163 | ); 164 | } 165 | ``` 166 | 167 | details see [examples/simple-tabs-starter](./examples/simple-tabs-starter) 168 | 169 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/irychen/keepalive-for-react/tree/main/examples/simple-tabs-starter) 170 | 171 | ## KeepAlive Props 172 | 173 | type definition 174 | 175 | ```tsx 176 | interface KeepAliveProps { 177 | // determine which component to is active 178 | activeCacheKey: string; 179 | children?: KeepAliveChildren; 180 | /** 181 | * max cache count default 10 182 | */ 183 | max?: number; 184 | exclude?: Array | string | RegExp; 185 | include?: Array | string | RegExp; 186 | onBeforeActive?: (activeCacheKey: string) => void; 187 | customContainerRef?: RefObject; 188 | cacheNodeClassName?: string; 189 | containerClassName?: string; 190 | errorElement?: ComponentType<{ 191 | children: ReactNode; 192 | }>; 193 | /** 194 | * transition default false 195 | */ 196 | transition?: boolean; 197 | /** 198 | * use view transition to animate the component when switching tabs 199 | * @see https://developer.chrome.com/docs/web-platform/view-transitions/ 200 | */ 201 | viewTransition?: boolean; 202 | /** 203 | * transition duration default 200 204 | */ 205 | duration?: number; 206 | aliveRef?: RefObject; 207 | /** 208 | * max alive time for cache node (second) 209 | * @default 0 (no limit) 210 | */ 211 | maxAliveTime?: number | MaxAliveConfig[]; 212 | } 213 | 214 | interface MaxAliveConfig { 215 | match: string | RegExp; 216 | expire: number; 217 | } 218 | ``` 219 | 220 | ## Hooks 221 | 222 | ### useEffectOnActive 223 | 224 | ```tsx 225 | useEffectOnActive(() => { 226 | console.log("active"); 227 | }, []); 228 | ``` 229 | 230 | ### useLayoutEffectOnActive 231 | 232 | ```tsx 233 | useLayoutEffectOnActive( 234 | () => { 235 | console.log("active"); 236 | }, 237 | [], 238 | false, 239 | ); 240 | // the third parameter is optional, default is false, 241 | // if true, which means the callback will be skipped when the useLayoutEffect is triggered in first render 242 | ``` 243 | 244 | ### useKeepAliveContext 245 | 246 | type definition 247 | 248 | ```ts 249 | interface KeepAliveContext { 250 | /** 251 | * whether the component is active 252 | */ 253 | active: boolean; 254 | /** 255 | * refresh the component 256 | * @param {string} [cacheKey] - The cache key of the component. If not provided, the current cached component will be refreshed. 257 | */ 258 | refresh: (cacheKey?: string) => void; 259 | /** 260 | * destroy the component 261 | * @param {string} [cacheKey] - the cache key of the component, if not provided, current active cached component will be destroyed 262 | */ 263 | destroy: (cacheKey?: string | string[]) => Promise; 264 | /** 265 | * destroy all components 266 | */ 267 | destroyAll: () => Promise; 268 | /** 269 | * destroy other components except the provided cacheKey 270 | * @param {string} [cacheKey] - The cache key of the component. If not provided, destroy all components except the current active cached component. 271 | */ 272 | destroyOther: (cacheKey?: string) => Promise; 273 | /** 274 | * get the cache nodes 275 | */ 276 | getCacheNodes: () => Array; 277 | } 278 | ``` 279 | 280 | ```tsx 281 | const { active, refresh, destroy, getCacheNodes } = useKeepAliveContext(); 282 | // active is a boolean, true is active, false is inactive 283 | // refresh is a function, you can call it to refresh the component 284 | // destroy is a function, you can call it to destroy the component 285 | // ... 286 | // getCacheNodes is a function, you can call it to get the cache nodes 287 | ``` 288 | 289 | ### useKeepAliveRef 290 | 291 | type definition 292 | 293 | ```ts 294 | interface KeepAliveRef { 295 | refresh: (cacheKey?: string) => void; 296 | destroy: (cacheKey?: string | string[]) => Promise; 297 | destroyAll: () => Promise; 298 | destroyOther: (cacheKey?: string) => Promise; 299 | getCacheNodes: () => Array; 300 | } 301 | ``` 302 | 303 | ```tsx 304 | function App() { 305 | const aliveRef = useKeepAliveRef(); 306 | // aliveRef.current is a KeepAliveRef object 307 | 308 | // you can call refresh and destroy on aliveRef.current 309 | aliveRef.current?.refresh(); 310 | // it is not necessary to call destroy manually, KeepAlive will handle it automatically 311 | aliveRef.current?.destroy(); 312 | 313 | return {/* ... */}; 314 | } 315 | // or 316 | function AppRouter() { 317 | const aliveRef = useKeepAliveRef(); 318 | // aliveRef.current is a KeepAliveRef object 319 | 320 | // you can call refresh and destroy on aliveRef.current 321 | aliveRef.current?.refresh(); 322 | aliveRef.current?.destroy(); 323 | return ; 324 | } 325 | ``` 326 | 327 | ## Development 328 | 329 | install dependencies 330 | 331 | ```bash 332 | pnpm install 333 | ``` 334 | 335 | build package 336 | 337 | ```bash 338 | pnpm build 339 | ``` 340 | 341 | link package to global 342 | 343 | ```bash 344 | pnpm link --global 345 | ``` 346 | 347 | test in demo project 348 | 349 | ```bash 350 | cd demo 351 | pnpm link --global keepalive-for-react 352 | ``` 353 | -------------------------------------------------------------------------------- /packages/core/README.zh_CN.md: -------------------------------------------------------------------------------- 1 |

2 | keepalive-for-react logo 3 |

4 | 5 |
6 |

7 | React KeepAlive 组件 8 |

9 |
10 | 11 |

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

12 | 13 | [English](./README.md) | 中文 14 | 15 | [![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) 16 | 17 | ## 包信息 18 | 19 | | 包名 | 版本 | 描述 | 20 | | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | 21 | | [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功能 | 22 | | [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集成 | 23 | 24 | ## 特性 25 | 26 | - 支持react-router-dom v6+ 或 react-router v7+ 27 | - 支持React v16+ ~ v18+ 28 | - 支持Suspense和懒加载导入 29 | - 支持错误边界 30 | - 支持自定义容器 31 | - 支持使用className `active`和`inactive`进行切换动画过渡 32 | - 简单实现,无需任何额外依赖和hack方式 33 | - 压缩后仅6KB大小 34 | 35 | ## 注意事项 36 | 37 | - 请勿使用 ,它在开发模式下无法与keepalive-for-react一起工作。因为它可能会导致一些意外行为。 38 | 39 | - 在路由中仅支持react-router-dom v6+ 40 | 41 | ## 安装 42 | 43 | ```bash 44 | npm install keepalive-for-react 45 | ``` 46 | 47 | ```bash 48 | yarn add keepalive-for-react 49 | ``` 50 | 51 | ```bash 52 | pnpm add keepalive-for-react 53 | ``` 54 | 55 | ## 使用 56 | 57 | ### 配合react-router-dom v6+ 或 react-router v7+使用 58 | 59 | 1. 安装react-router-dom v6+ 或 react-router v7+ 60 | 61 | ```bash 62 | # v6+ 63 | npm install react-router-dom keepalive-for-react keepalive-for-react-router@1.x.x 64 | # v7+ 65 | npm install react-router keepalive-for-react keepalive-for-react-router@2.x.x 66 | ``` 67 | 68 | 2. 在项目中使用KeepAlive 69 | 70 | ```tsx 71 | // v6+ keepalive-for-react-router@1.x.x 72 | // v7+ keepalive-for-react-router@2.x.x 73 | import KeepAliveRouteOutlet from "keepalive-for-react-router"; 74 | 75 | function Layout() { 76 | return ( 77 |
78 | 79 |
80 | ); 81 | } 82 | ``` 83 | 84 | 或者 85 | 86 | ```tsx 87 | import { useMemo } from "react"; 88 | import { useLocation } from "react-router-dom"; 89 | import { KeepAlive, useKeepAliveRef } from "keepalive-for-react"; 90 | 91 | function Layout() { 92 | const location = useLocation(); 93 | const aliveRef = useKeepAliveRef(); 94 | 95 | const outlet = useOutlet(); 96 | 97 | // 确定哪个路由组件处于活动状态 98 | const currentCacheKey = useMemo(() => { 99 | return location.pathname + location.search; 100 | }, [location.pathname, location.search]); 101 | 102 | return ( 103 |
104 | 105 | 106 | }> 107 | {outlet} 108 | 109 | 110 | 111 |
112 | ); 113 | } 114 | ``` 115 | 116 | 详情请参见 [examples/react-router-dom-simple-starter](./examples/react-router-dom-simple-starter) 117 | 118 | [![在StackBlitz中打开](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/irychen/keepalive-for-react/tree/main/examples/react-router-dom-simple-starter) 119 | 120 | ### 在简单标签页中 121 | 122 | ```bash 123 | npm install keepalive-for-react 124 | ``` 125 | 126 | ```tsx 127 | const tabs = [ 128 | { 129 | key: "tab1", 130 | label: "标签1", 131 | component: Tab1, 132 | }, 133 | { 134 | key: "tab2", 135 | label: "标签2", 136 | component: Tab2, 137 | }, 138 | { 139 | key: "tab3", 140 | label: "标签3", 141 | component: Tab3, 142 | }, 143 | ]; 144 | 145 | function App() { 146 | const [currentTab, setCurrentTab] = useState("tab1"); 147 | 148 | const tab = useMemo(() => { 149 | return tabs.find(tab => tab.key === currentTab); 150 | }, [currentTab]); 151 | 152 | return ( 153 |
154 | {/* ... */} 155 | 156 | {tab && } 157 | 158 |
159 | ); 160 | } 161 | ``` 162 | 163 | 详情请参见 [examples/simple-tabs-starter](./examples/simple-tabs-starter) 164 | 165 | [![在StackBlitz中打开](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/irychen/keepalive-for-react/tree/main/examples/simple-tabs-starter) 166 | 167 | ## KeepAlive 属性 168 | 169 | 类型定义 170 | 171 | ```tsx 172 | interface KeepAliveProps { 173 | // 确定哪个组件处于活动状态 174 | activeCacheKey: string; 175 | children?: KeepAliveChildren; 176 | /** 177 | * 最大缓存数量 默认10 178 | */ 179 | max?: number; 180 | exclude?: Array | string | RegExp; 181 | include?: Array | string | RegExp; 182 | onBeforeActive?: (activeCacheKey: string) => void; 183 | customContainerRef?: RefObject; 184 | cacheNodeClassName?: string; 185 | containerClassName?: string; 186 | errorElement?: ComponentType<{ 187 | children: ReactNode; 188 | }>; 189 | /** 190 | * 过渡效果 默认false 191 | */ 192 | transition?: boolean; 193 | 194 | /** 195 | * 使用view transition来过渡组件 默认false 196 | * @see https://developer.chrome.com/docs/web-platform/view-transitions/ 197 | */ 198 | viewTransition?: boolean; 199 | 200 | /** 201 | * 过渡时间 默认200ms 202 | */ 203 | duration?: number; 204 | aliveRef?: RefObject; 205 | /** 206 | * 缓存节点最大存活时间 (秒) 207 | * @default 0 (无限制) 208 | */ 209 | maxAliveTime?: number | MaxAliveConfig[]; 210 | } 211 | 212 | interface MaxAliveConfig { 213 | match: string | RegExp; 214 | expire: number; 215 | } 216 | ``` 217 | 218 | ## Hooks 219 | 220 | ### useEffectOnActive 221 | 222 | ```tsx 223 | useEffectOnActive(() => { 224 | console.log("active"); 225 | }, []); 226 | ``` 227 | 228 | ### useLayoutEffectOnActive 229 | 230 | ```tsx 231 | useLayoutEffectOnActive( 232 | () => { 233 | console.log("active"); 234 | }, 235 | [], 236 | false, 237 | ); 238 | // 第三个参数是可选的,默认为false, 239 | // 如果为true,表示在首次渲染时触发useLayoutEffect时会跳过回调 240 | ``` 241 | 242 | ### useKeepAliveContext 243 | 244 | 类型定义 245 | 246 | ```ts 247 | interface KeepAliveContext { 248 | /** 249 | * 组件是否处于活动状态 250 | */ 251 | active: boolean; 252 | /** 253 | * 刷新组件 254 | * @param {string} [cacheKey] - 组件的缓存键。如果未提供,将刷新当前缓存的组件。 255 | */ 256 | refresh: (cacheKey?: string) => void; 257 | /** 258 | * 销毁组件 259 | * @param {string} [cacheKey] - 组件的缓存键,如果未提供,将销毁当前活动的缓存组件。 260 | */ 261 | destroy: (cacheKey?: string | string[]) => Promise; 262 | /** 263 | * 销毁所有组件 264 | */ 265 | destroyAll: () => Promise; 266 | /** 267 | * 销毁除提供的cacheKey外的其他组件 268 | * @param {string} [cacheKey] - 组件的缓存键。如果未提供,将销毁除当前活动缓存组件外的所有组件。 269 | */ 270 | destroyOther: (cacheKey?: string) => Promise; 271 | /** 272 | * 获取缓存节点 273 | */ 274 | getCacheNodes: () => Array; 275 | } 276 | ``` 277 | 278 | ```tsx 279 | const { active, refresh, destroy, getCacheNodes } = useKeepAliveContext(); 280 | // active 是一个布尔值,true表示活动,false表示非活动 281 | // refresh 是一个函数,你可以调用它来刷新组件 282 | // destroy 是一个函数,你可以调用它来销毁组件 283 | // ... 284 | // getCacheNodes 是一个函数,你可以调用它来获取缓存节点 285 | ``` 286 | 287 | ### useKeepAliveRef 288 | 289 | 类型定义 290 | 291 | ```ts 292 | interface KeepAliveRef { 293 | refresh: (cacheKey?: string) => void; 294 | destroy: (cacheKey?: string | string[]) => Promise; 295 | destroyAll: () => Promise; 296 | destroyOther: (cacheKey?: string) => Promise; 297 | getCacheNodes: () => Array; 298 | } 299 | ``` 300 | 301 | ```tsx 302 | function App() { 303 | const aliveRef = useKeepAliveRef(); 304 | // aliveRef.current 是一个 KeepAliveRef 对象 305 | 306 | // 你可以在 aliveRef.current 上调用 refresh 和 destroy 307 | aliveRef.current?.refresh(); 308 | // 通常不需要手动调用 destroy,KeepAlive 会自动处理 309 | aliveRef.current?.destroy(); 310 | 311 | return {/* ... */}; 312 | } 313 | // 或者 314 | function AppRouter() { 315 | const aliveRef = useKeepAliveRef(); 316 | // aliveRef.current 是一个 KeepAliveRef 对象 317 | 318 | // 你可以在 aliveRef.current 上调用 refresh 和 destroy 319 | aliveRef.current?.refresh(); 320 | aliveRef.current?.destroy(); 321 | return ; 322 | } 323 | ``` 324 | 325 | ## 开发 326 | 327 | 安装依赖 328 | 329 | ```bash 330 | pnpm install 331 | ``` 332 | 333 | 构建包 334 | 335 | ```bash 336 | pnpm build 337 | ``` 338 | 339 | 链接包到全局 340 | 341 | ```bash 342 | pnpm link --global 343 | ``` 344 | 345 | 在演示项目中测试 346 | 347 | ```bash 348 | cd demo 349 | pnpm link --global keepalive-for-react 350 | ``` 351 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keepalive-for-react", 3 | "version": "4.0.2", 4 | "description": "A react component like in vue", 5 | "homepage": "https://github.com/irychen/keepalive-for-react", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/irychen/keepalive-for-react.git" 9 | }, 10 | "exports": { 11 | ".": { 12 | "types": "./dist/types/index.d.ts", 13 | "development": { 14 | "import": "./dist/esm/index.mjs", 15 | "require": "./dist/cjs/index.cjs" 16 | }, 17 | "production": { 18 | "import": "./dist/esm/index-min.mjs", 19 | "require": "./dist/cjs/index-min.cjs" 20 | }, 21 | "default": { 22 | "import": "./dist/esm/index-min.mjs", 23 | "require": "./dist/cjs/index-min.cjs" 24 | } 25 | } 26 | }, 27 | "module": "./dist/esm/index.mjs", 28 | "main": "./dist/cjs/index.cjs", 29 | "types": "./dist/types/index.d.ts", 30 | "files": [ 31 | "dist/types/**/*.d.ts", 32 | "dist/esm/index.mjs", 33 | "dist/esm/index-min.mjs", 34 | "dist/cjs/index.cjs", 35 | "dist/cjs/index-min.cjs" 36 | ], 37 | "type": "module", 38 | "scripts": { 39 | "build:main": "rollup --config rollup.config.js", 40 | "build:types": "tsc -b ./tsconfig.types.json", 41 | "build": "npm run build:main && npm run build:types", 42 | "clean": "rm -rf dist" 43 | }, 44 | "keywords": [ 45 | "keepalive", 46 | "keep-alive", 47 | "react keepalive", 48 | "keepalive for react", 49 | "keepalive-for-react" 50 | ], 51 | "author": "wongyichen", 52 | "license": "MIT", 53 | "peerDependencies": { 54 | "react": ">=16.8.0", 55 | "react-dom": ">=16.8.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/core/react-keepalive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/irychen/keepalive-for-react/bde1c3feb4d53e54b8840ecf85064179757bdb64/packages/core/react-keepalive.png -------------------------------------------------------------------------------- /packages/core/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 2 | import typescript from "@rollup/plugin-typescript"; 3 | import commonjs from "@rollup/plugin-commonjs"; 4 | import terser from "@rollup/plugin-terser"; 5 | import babel from "@rollup/plugin-babel"; 6 | 7 | const commonConfig = { 8 | input: "src/index.ts", 9 | external: ["react", "react-dom", "react/jsx-runtime"], 10 | plugins: [ 11 | nodeResolve(), 12 | babel({ 13 | exclude: "node_modules/**", 14 | babelHelpers: "bundled", 15 | }), 16 | typescript(), 17 | commonjs(), 18 | ], 19 | }; 20 | 21 | const config = [ 22 | { 23 | ...commonConfig, 24 | output: { 25 | file: "dist/esm/index.mjs", 26 | exports: "named", 27 | format: "esm", 28 | }, 29 | }, 30 | { 31 | ...commonConfig, 32 | output: { 33 | file: "dist/esm/index-min.mjs", 34 | exports: "named", 35 | format: "esm", 36 | }, 37 | plugins: [...commonConfig.plugins, terser()], 38 | }, 39 | { 40 | ...commonConfig, 41 | output: { 42 | file: "dist/cjs/index.cjs", 43 | exports: "named", 44 | format: "cjs", 45 | }, 46 | }, 47 | { 48 | ...commonConfig, 49 | output: { 50 | file: "dist/cjs/index-min.cjs", 51 | exports: "named", 52 | format: "cjs", 53 | }, 54 | plugins: [...commonConfig.plugins, terser()], 55 | }, 56 | ]; 57 | export default config; 58 | -------------------------------------------------------------------------------- /packages/core/src/compat/safeStartTransition.ts: -------------------------------------------------------------------------------- 1 | import { startTransition as reactStartTransition } from "react"; 2 | import { isFn } from "../utils"; 3 | 4 | /** 5 | * Compatible with React versions < 18 startTransition 6 | * @param cb Callback function to be executed in transition 7 | */ 8 | const safeStartTransition = (cb: () => void): void => { 9 | if (typeof reactStartTransition !== "undefined" && isFn(reactStartTransition)) { 10 | reactStartTransition(cb); 11 | } else { 12 | cb(); 13 | } 14 | }; 15 | 16 | export default safeStartTransition; 17 | -------------------------------------------------------------------------------- /packages/core/src/components/CacheComponent/index.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentType, Fragment, memo, ReactNode, RefObject, useEffect, useMemo, useRef } from "react"; 2 | import { createPortal } from "react-dom"; 3 | import { delayAsync, domAttrSet, isInclude } from "../../utils"; 4 | 5 | export interface CacheComponentProps { 6 | children: ReactNode; 7 | errorElement?: ComponentType<{ 8 | children: ReactNode; 9 | }>; 10 | containerDivRef: RefObject; 11 | cacheNodeClassName: string; 12 | renderCount: number; 13 | active: boolean; 14 | cacheKey: string; 15 | transition: boolean; 16 | viewTransition: boolean; 17 | duration: number; 18 | exclude?: Array | string | RegExp; 19 | include?: Array | string | RegExp; 20 | destroy: (cacheKey: string | string[]) => Promise; 21 | } 22 | 23 | const cacheDivMarkedClassName = "keepalive-cache-div"; 24 | 25 | function getChildNodes(dom?: HTMLDivElement) { 26 | return dom ? Array.from(dom.children) : []; 27 | } 28 | 29 | function removeDivNodes(nodes: Element[]) { 30 | nodes.forEach(node => { 31 | if (node.classList.contains(cacheDivMarkedClassName)) { 32 | node.remove(); 33 | } 34 | }); 35 | } 36 | 37 | function renderCacheDiv(containerDiv: HTMLDivElement, cacheDiv: HTMLDivElement) { 38 | const removeNodes = getChildNodes(containerDiv); 39 | removeDivNodes(removeNodes); 40 | containerDiv.appendChild(cacheDiv); 41 | cacheDiv.classList.remove("inactive"); 42 | cacheDiv.classList.add("active"); 43 | } 44 | 45 | function switchActiveNodesToInactive(containerDiv: HTMLDivElement, cacheKey: string) { 46 | const nodes = getChildNodes(containerDiv); 47 | const activeNodes = nodes.filter(node => node.classList.contains("active") && node.getAttribute("data-cache-key") !== cacheKey); 48 | activeNodes.forEach(node => { 49 | node.classList.remove("active"); 50 | node.classList.add("inactive"); 51 | }); 52 | return activeNodes; 53 | } 54 | 55 | function isCached( 56 | cacheKey: string, 57 | exclude?: Array | string | RegExp, 58 | include?: Array | string | RegExp, 59 | ) { 60 | if (include) { 61 | return isInclude(include, cacheKey); 62 | } else { 63 | if (exclude) { 64 | return !isInclude(exclude, cacheKey); 65 | } 66 | return true; 67 | } 68 | } 69 | 70 | const CacheComponent = memo( 71 | function (props: CacheComponentProps): any { 72 | const { errorElement: ErrorBoundary = Fragment, cacheNodeClassName, children, cacheKey, exclude, include } = props; 73 | const { active, renderCount, destroy, transition, viewTransition, duration, containerDivRef } = props; 74 | const activatedRef = useRef(false); 75 | 76 | activatedRef.current = activatedRef.current || active; 77 | 78 | const cacheDiv = useMemo(() => { 79 | const cacheDiv = document.createElement("div"); 80 | domAttrSet(cacheDiv) 81 | .set("data-cache-key", cacheKey) 82 | .set("style", "height: 100%") 83 | .set("data-render-count", renderCount.toString()); 84 | cacheDiv.className = cacheDivMarkedClassName + (cacheNodeClassName ? ` ${cacheNodeClassName}` : ""); 85 | return cacheDiv; 86 | }, [renderCount, cacheNodeClassName]); 87 | 88 | useEffect(() => { 89 | const cached = isCached(cacheKey, exclude, include); 90 | const containerDiv = containerDivRef.current; 91 | if (!containerDiv) { 92 | console.warn(`keepalive: cache container not found`); 93 | return; 94 | } 95 | if (transition) { 96 | (async () => { 97 | if (active) { 98 | const inactiveNodes = switchActiveNodesToInactive(containerDiv, cacheKey); 99 | // duration - 40ms is to avoid the animation effect ending too early 100 | await delayAsync(duration - 40); 101 | removeDivNodes(inactiveNodes); 102 | if (containerDiv.contains(cacheDiv)) { 103 | return; 104 | } 105 | renderCacheDiv(containerDiv, cacheDiv); 106 | } else { 107 | if (!cached) { 108 | await delayAsync(duration); 109 | destroy(cacheKey); 110 | } 111 | } 112 | })(); 113 | } else { 114 | if (active) { 115 | const makeChange = () => { 116 | const inactiveNodes = switchActiveNodesToInactive(containerDiv, cacheKey); 117 | removeDivNodes(inactiveNodes); 118 | if (containerDiv.contains(cacheDiv)) { 119 | return; 120 | } 121 | renderCacheDiv(containerDiv, cacheDiv); 122 | }; 123 | if (viewTransition && (document as any).startViewTransition) { 124 | (document as any).startViewTransition(makeChange); 125 | } else { 126 | makeChange(); 127 | } 128 | } else { 129 | if (!cached) { 130 | destroy(cacheKey); 131 | } 132 | } 133 | } 134 | }, [active, containerDivRef, cacheKey, exclude, include]); 135 | 136 | return activatedRef.current ? createPortal({children}, cacheDiv, cacheKey) : null; 137 | }, 138 | (prevProps, nextProps) => { 139 | return ( 140 | prevProps.active === nextProps.active && 141 | prevProps.renderCount === nextProps.renderCount && 142 | prevProps.children === nextProps.children && 143 | prevProps.exclude === nextProps.exclude && 144 | prevProps.include === nextProps.include 145 | ); 146 | }, 147 | ); 148 | 149 | export default CacheComponent; 150 | -------------------------------------------------------------------------------- /packages/core/src/components/CacheComponentProvider/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo, ReactNode, useMemo } from "react"; 2 | import { CacheComponentContext, KeepAliveContext } from "../CacheContext"; 3 | 4 | interface CacheComponentProviderProps extends KeepAliveContext { 5 | children?: ReactNode; 6 | } 7 | 8 | const CacheComponentProvider = memo(function (props: CacheComponentProviderProps) { 9 | const { children, active, refresh, destroy, destroyAll, destroyOther, getCacheNodes } = props; 10 | const value = useMemo(() => { 11 | return { active, refresh, destroy, destroyAll, destroyOther, getCacheNodes }; 12 | }, [active, refresh, destroy, destroyAll, destroyOther, getCacheNodes]); 13 | return {children}; 14 | }); 15 | 16 | export default CacheComponentProvider; 17 | -------------------------------------------------------------------------------- /packages/core/src/components/CacheContext/index.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | import { KeepAliveAPI } from "../KeepAlive"; 3 | 4 | export interface KeepAliveContext extends KeepAliveAPI { 5 | /** 6 | * whether the component is active 7 | */ 8 | active: boolean; 9 | } 10 | 11 | export const CacheComponentContext = createContext({ 12 | active: false, 13 | refresh: () => {}, 14 | destroy: () => Promise.resolve(), 15 | destroyAll: () => Promise.resolve(), 16 | destroyOther: () => Promise.resolve(), 17 | getCacheNodes: () => [], 18 | }); 19 | -------------------------------------------------------------------------------- /packages/core/src/components/KeepAlive/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ComponentType, 3 | Fragment, 4 | ReactElement, 5 | ReactNode, 6 | RefObject, 7 | useCallback, 8 | useImperativeHandle, 9 | useLayoutEffect, 10 | useRef, 11 | useState, 12 | } from "react"; 13 | import { isArr, isFn, isNil, isRegExp, macroTask } from "../../utils"; 14 | import CacheComponentProvider from "../CacheComponentProvider"; 15 | import CacheComponent from "../CacheComponent"; 16 | import safeStartTransition from "../../compat/safeStartTransition"; 17 | 18 | export type KeepAliveChildren = ReactNode | ReactElement | null | undefined | JSX.Element; 19 | 20 | export interface KeepAliveProps { 21 | activeCacheKey: string; 22 | children?: KeepAliveChildren; 23 | /** 24 | * max cache count default 10 25 | */ 26 | max?: number; 27 | exclude?: Array | string | RegExp; 28 | include?: Array | string | RegExp; 29 | onBeforeActive?: (activeCacheKey: string) => void; 30 | customContainerRef?: RefObject; 31 | cacheNodeClassName?: string; 32 | containerClassName?: string; 33 | errorElement?: ComponentType<{ 34 | children: ReactNode; 35 | }>; 36 | /** 37 | * transition default false 38 | */ 39 | transition?: boolean; 40 | 41 | /** 42 | * view transition default false 43 | * 44 | * use viewTransition to animate the component when switching tabs 45 | * 46 | * @see https://developer.chrome.com/docs/web-platform/view-transitions/ 47 | */ 48 | viewTransition?: boolean; 49 | /** 50 | * transition duration default 200 51 | */ 52 | duration?: number; 53 | aliveRef?: RefObject; 54 | /** 55 | * max alive time for cache node (second) 56 | * @default 0 (no limit) 57 | */ 58 | maxAliveTime?: number | MaxAliveConfig[]; 59 | } 60 | 61 | interface MaxAliveConfig { 62 | match: string | RegExp; 63 | expire: number; 64 | } 65 | 66 | export interface CacheNode { 67 | cacheKey: string; 68 | ele?: KeepAliveChildren; 69 | lastActiveTime: number; 70 | renderCount: number; 71 | } 72 | 73 | export interface KeepAliveAPI { 74 | /** 75 | * Refreshes the component. 76 | * @param {string} [cacheKey] - The cache key of the component. If not provided, the current cached component will be refreshed. 77 | */ 78 | refresh: (cacheKey?: string) => void; 79 | /** 80 | * destroy the component 81 | * @param {string} [cacheKey] - the cache key of the component, if not provided, current active cached component will be destroyed 82 | */ 83 | destroy: (cacheKey?: string | string[]) => Promise; 84 | /** 85 | * destroy all components 86 | */ 87 | destroyAll: () => Promise; 88 | /** 89 | * destroy other components except the provided cacheKey 90 | * @param {string} [cacheKey] - The cache key of the component. If not provided, destroy all components except the current active cached component. 91 | */ 92 | destroyOther: (cacheKey?: string) => Promise; 93 | /** 94 | * get the cache nodes 95 | */ 96 | getCacheNodes: () => Array; 97 | } 98 | 99 | export interface KeepAliveRef extends KeepAliveAPI {} 100 | 101 | export function useKeepAliveRef() { 102 | return useRef(); 103 | } 104 | 105 | function KeepAlive(props: KeepAliveProps) { 106 | const { 107 | activeCacheKey, 108 | max = 10, 109 | exclude, 110 | include, 111 | onBeforeActive, 112 | customContainerRef, 113 | cacheNodeClassName = `cache-component`, 114 | containerClassName = "keep-alive-render", 115 | errorElement, 116 | transition = false, 117 | viewTransition = false, 118 | duration = 200, 119 | children, 120 | aliveRef, 121 | maxAliveTime = 0, 122 | } = props; 123 | 124 | const containerDivRef = customContainerRef || useRef(null); 125 | const [cacheNodes, setCacheNodes] = useState>([]); 126 | 127 | useLayoutEffect(() => { 128 | if (isNil(activeCacheKey)) return; 129 | safeStartTransition(() => { 130 | setCacheNodes(prevCacheNodes => { 131 | const lastActiveTime = Date.now(); 132 | const cacheNode = prevCacheNodes.find(item => item.cacheKey === activeCacheKey); 133 | if (cacheNode) { 134 | return prevCacheNodes.map(item => { 135 | if (item.cacheKey === activeCacheKey) { 136 | let needUpdate = false; 137 | if (isFn(onBeforeActive)) onBeforeActive(activeCacheKey); 138 | if (maxAliveTime) { 139 | const prev = item.lastActiveTime; 140 | if (isArr(maxAliveTime)) { 141 | const config = maxAliveTime.find(item => { 142 | return isRegExp(item.match) ? item.match.test(activeCacheKey) : item.match === activeCacheKey; 143 | }); 144 | if (config) { 145 | needUpdate = config && prev + config.expire * 1000 < lastActiveTime; 146 | } 147 | } else { 148 | needUpdate = prev + maxAliveTime * 1000 < lastActiveTime; 149 | } 150 | } 151 | return { 152 | ...item, 153 | ele: children, 154 | lastActiveTime, 155 | renderCount: needUpdate ? item.renderCount + 1 : item.renderCount, 156 | }; 157 | } 158 | return item; 159 | }); 160 | } else { 161 | if (isFn(onBeforeActive)) onBeforeActive(activeCacheKey); 162 | if (prevCacheNodes.length > max) { 163 | const node = prevCacheNodes.reduce((prev, cur) => { 164 | return prev.lastActiveTime < cur.lastActiveTime ? prev : cur; 165 | }); 166 | prevCacheNodes.splice(prevCacheNodes.indexOf(node), 1); 167 | } 168 | return [...prevCacheNodes, { cacheKey: activeCacheKey, lastActiveTime, ele: children, renderCount: 0 }]; 169 | } 170 | }); 171 | }); 172 | }, [activeCacheKey, children]); 173 | 174 | const refresh = useCallback( 175 | (cacheKey?: string) => { 176 | setCacheNodes(cacheNodes => { 177 | const targetCacheKey = cacheKey || activeCacheKey; 178 | return cacheNodes.map(item => { 179 | if (item.cacheKey === targetCacheKey) { 180 | return { ...item, renderCount: item.renderCount + 1 }; 181 | } 182 | return item; 183 | }); 184 | }); 185 | }, 186 | [setCacheNodes, activeCacheKey], 187 | ); 188 | 189 | const destroy = useCallback( 190 | (cacheKey?: string | string[]) => { 191 | const targetCacheKey = cacheKey || activeCacheKey; 192 | const cacheKeys = isArr(targetCacheKey) ? targetCacheKey : [targetCacheKey]; 193 | return new Promise(resolve => { 194 | macroTask(() => { 195 | setCacheNodes(cacheNodes => { 196 | return [...cacheNodes.filter(item => !cacheKeys.includes(item.cacheKey))]; 197 | }); 198 | resolve(); 199 | }); 200 | }); 201 | }, 202 | [setCacheNodes, activeCacheKey], 203 | ); 204 | 205 | const destroyAll = useCallback(() => { 206 | return new Promise(resolve => { 207 | macroTask(() => { 208 | setCacheNodes([]); 209 | resolve(); 210 | }); 211 | }); 212 | }, [setCacheNodes]); 213 | 214 | const destroyOther = useCallback( 215 | (cacheKey?: string) => { 216 | const targetCacheKey = cacheKey || activeCacheKey; 217 | return new Promise(resolve => { 218 | macroTask(() => { 219 | setCacheNodes(cacheNodes => { 220 | return [...cacheNodes.filter(item => item.cacheKey === targetCacheKey)]; 221 | }); 222 | resolve(); 223 | }); 224 | }); 225 | }, 226 | [activeCacheKey, setCacheNodes], 227 | ); 228 | 229 | const getCacheNodes = useCallback(() => { 230 | return cacheNodes; 231 | }, [cacheNodes]); 232 | 233 | useImperativeHandle(aliveRef, () => ({ 234 | refresh, 235 | destroy, 236 | destroyAll, 237 | destroyOther, 238 | getCacheNodes, 239 | })); 240 | 241 | return ( 242 | 243 |
244 | {cacheNodes.map(item => { 245 | const { cacheKey, ele, renderCount } = item; 246 | return ( 247 | 256 | 270 | {ele} 271 | 272 | 273 | ); 274 | })} 275 |
276 | ); 277 | } 278 | 279 | export default KeepAlive; 280 | -------------------------------------------------------------------------------- /packages/core/src/hooks/useEffectOnActive.ts: -------------------------------------------------------------------------------- 1 | import { DependencyList, useEffect } from "react"; 2 | import useOnActive from "./useOnActive"; 3 | 4 | const useEffectOnActive = (cb: () => void, deps: DependencyList, skipMount = false): void => { 5 | useOnActive(cb, deps, skipMount, useEffect); 6 | }; 7 | 8 | export default useEffectOnActive; 9 | -------------------------------------------------------------------------------- /packages/core/src/hooks/useKeepAliveContext.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { CacheComponentContext } from "../components/CacheContext"; 3 | 4 | const useKeepAliveContext = () => { 5 | return useContext(CacheComponentContext); 6 | }; 7 | 8 | export default useKeepAliveContext; 9 | -------------------------------------------------------------------------------- /packages/core/src/hooks/useLayoutEffectOnActive.ts: -------------------------------------------------------------------------------- 1 | import { DependencyList, useLayoutEffect } from "react"; 2 | import useOnActive from "./useOnActive"; 3 | 4 | const useLayoutEffectOnActive = (cb: () => any, deps: DependencyList, skipMount = false): void => { 5 | useOnActive(cb, deps, skipMount, useLayoutEffect); 6 | }; 7 | 8 | export default useLayoutEffectOnActive; 9 | -------------------------------------------------------------------------------- /packages/core/src/hooks/useOnActive.ts: -------------------------------------------------------------------------------- 1 | import { DependencyList, useEffect, useLayoutEffect, useRef } from "react"; 2 | import useKeepAliveContext from "./useKeepAliveContext"; 3 | import { isFn } from "../utils"; 4 | 5 | function useOnActive(cb: () => any, deps: DependencyList, skipMount = false, effect: typeof useEffect | typeof useLayoutEffect) { 6 | const { active } = useKeepAliveContext(); 7 | const isMount = useRef(false); 8 | effect(() => { 9 | if (!active) return; 10 | if (skipMount && !isMount.current) { 11 | isMount.current = true; 12 | return; 13 | } 14 | const destroyCb = cb(); 15 | return () => { 16 | if (isFn(destroyCb)) { 17 | destroyCb(); 18 | } 19 | }; 20 | }, [active, ...deps]); 21 | } 22 | 23 | export default useOnActive; 24 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | import KeepAlive, { KeepAliveProps, KeepAliveRef, useKeepAliveRef } from "./components/KeepAlive"; 2 | import useEffectOnActive from "./hooks/useEffectOnActive"; 3 | import useKeepAliveContext from "./hooks/useKeepAliveContext"; 4 | import useLayoutEffectOnActive from "./hooks/useLayoutEffectOnActive"; 5 | 6 | /** 7 | * @deprecated since version 3.0.2. Use `useKeepAliveRef` instead. 8 | */ 9 | const useKeepaliveRef = useKeepAliveRef; 10 | 11 | export { 12 | KeepAlive as default, 13 | KeepAlive, 14 | useKeepAliveRef, 15 | useKeepaliveRef, 16 | useEffectOnActive, 17 | useLayoutEffectOnActive, 18 | useKeepAliveContext, 19 | }; 20 | 21 | export type { KeepAliveRef, KeepAliveProps }; 22 | -------------------------------------------------------------------------------- /packages/core/src/utils/index.tsx: -------------------------------------------------------------------------------- 1 | export function isNil(value: any): value is null | undefined { 2 | return value === null || value === undefined; 3 | } 4 | 5 | export function isRegExp(value: any): value is RegExp { 6 | return Object.prototype.toString.call(value) === "[object RegExp]"; 7 | } 8 | 9 | export function isArr(value: any): value is Array { 10 | return Array.isArray(value); 11 | } 12 | 13 | export function isFn(value: any): value is Function { 14 | return typeof value === "function"; 15 | } 16 | 17 | export function domAttrSet(dom: HTMLDivElement) { 18 | return { 19 | set: (key: string, value: string) => { 20 | dom.setAttribute(key, value); 21 | return domAttrSet(dom); 22 | }, 23 | }; 24 | } 25 | 26 | export function delayAsync(milliseconds: number = 100): Promise { 27 | let _timeID: null | number | NodeJS.Timeout; 28 | return new Promise((resolve, _reject) => { 29 | _timeID = setTimeout(() => { 30 | resolve(); 31 | if (!isNil(_timeID)) { 32 | clearTimeout(_timeID); 33 | } 34 | }, milliseconds); 35 | }); 36 | } 37 | 38 | export function isInclude(include: Array | string | RegExp | undefined, val: string) { 39 | const includes = isArr(include) ? include : isNil(include) ? [] : [include]; 40 | return includes.some(include => { 41 | if (isRegExp(include)) { 42 | return include.test(val); 43 | } else { 44 | return val === include; 45 | } 46 | }); 47 | } 48 | 49 | export function macroTask(fn: () => void) { 50 | setTimeout(fn, 0); 51 | } 52 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "jsx": "react-jsx", 13 | /* Linting */ 14 | "strict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "allowSyntheticDefaultImports": true, 19 | "types": ["node"] 20 | }, 21 | "include": ["src/**/*"], 22 | "exclude": ["node_modules", "dist"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/core/tsconfig.types.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "es2015", 5 | "removeComments": false, 6 | "declaration": true, 7 | "declarationMap": false, 8 | "declarationDir": "./dist/types", 9 | "emitDeclarationOnly": true, 10 | "rootDir": "./src" 11 | }, 12 | "exclude": ["src/**/test.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/tsconfig.types.tsbuildinfo: -------------------------------------------------------------------------------- 1 | {"root":["./src/index.ts","./src/compat/safestarttransition.ts","./src/components/cachecomponent/index.tsx","./src/components/cachecomponentprovider/index.tsx","./src/components/cachecontext/index.tsx","./src/components/keepalive/index.tsx","./src/hooks/useeffectonactive.ts","./src/hooks/usekeepalivecontext.ts","./src/hooks/uselayouteffectonactive.ts","./src/hooks/useonactive.ts","./src/utils/index.tsx"],"version":"5.7.2"} -------------------------------------------------------------------------------- /packages/router/README.md: -------------------------------------------------------------------------------- 1 | # KeepAlive for React Router 2 | 3 | ## Installation 4 | 5 | ```bash 6 | npm install keepalive-for-react keepalive-for-react-router 7 | ``` 8 | 9 | ### v6+ 10 | 11 | ```bash 12 | npm install react-router-dom keepalive-for-react keepalive-for-react-router@1.x.x 13 | ``` 14 | 15 | ### v7+ 16 | 17 | ```bash 18 | npm install react-router keepalive-for-react keepalive-for-react-router@2.x.x 19 | ``` 20 | 21 | ## Usage 22 | 23 | ```tsx 24 | // v6+ keepalive-for-react-router@1.x.x 25 | // v7+ keepalive-for-react-router@2.x.x 26 | import KeepAliveRouteOutlet from "keepalive-for-react-router"; 27 | 28 | function Layout() { 29 | return ( 30 |
31 | 32 |
33 | ); 34 | } 35 | ``` 36 | -------------------------------------------------------------------------------- /packages/router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keepalive-for-react-router", 3 | "version": "2.0.2", 4 | "description": "React Router integration for keepalive-for-react", 5 | "homepage": "https://github.com/irychen/keepalive-for-react", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/irychen/keepalive-for-react.git" 9 | }, 10 | "exports": { 11 | ".": { 12 | "types": "./dist/types/index.d.ts", 13 | "development": { 14 | "import": "./dist/esm/index.mjs", 15 | "require": "./dist/cjs/index.cjs" 16 | }, 17 | "production": { 18 | "import": "./dist/esm/index-min.mjs", 19 | "require": "./dist/cjs/index-min.cjs" 20 | }, 21 | "default": { 22 | "import": "./dist/esm/index-min.mjs", 23 | "require": "./dist/cjs/index-min.cjs" 24 | } 25 | } 26 | }, 27 | "module": "./dist/esm/index.mjs", 28 | "main": "./dist/cjs/index.cjs", 29 | "types": "./dist/types/index.d.ts", 30 | "files": [ 31 | "dist/types/**/*.d.ts", 32 | "dist/esm/index.mjs", 33 | "dist/esm/index-min.mjs", 34 | "dist/cjs/index.cjs", 35 | "dist/cjs/index-min.cjs" 36 | ], 37 | "type": "module", 38 | "scripts": { 39 | "build:main": "rollup --config rollup.config.js", 40 | "build:types": "tsc -b ./tsconfig.types.json", 41 | "build": "npm run build:main && npm run build:types", 42 | "clean": "rm -rf dist" 43 | }, 44 | "keywords": [ 45 | "keepalive", 46 | "keep-alive", 47 | "react keepalive", 48 | "keepalive for react", 49 | "keepalive-for-react", 50 | "keepalive-for-react-router" 51 | ], 52 | "author": "wongyichen", 53 | "license": "MIT", 54 | "peerDependencies": { 55 | "keepalive-for-react": "^4.0.0", 56 | "react": ">=16.8.0", 57 | "react-dom": ">=16.8.0", 58 | "react-router": ">=7.0.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/router/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 2 | import typescript from "@rollup/plugin-typescript"; 3 | import commonjs from "@rollup/plugin-commonjs"; 4 | import terser from "@rollup/plugin-terser"; 5 | import babel from "@rollup/plugin-babel"; 6 | 7 | const commonConfig = { 8 | input: "src/index.ts", 9 | external: ["react", "react-dom", "react-router", "react/jsx-runtime", "keepalive-for-react"], 10 | plugins: [ 11 | nodeResolve(), 12 | babel({ 13 | exclude: "node_modules/**", 14 | babelHelpers: "bundled", 15 | }), 16 | typescript({ 17 | jsx: "react-jsx", 18 | }), 19 | commonjs(), 20 | ], 21 | }; 22 | 23 | const config = [ 24 | { 25 | ...commonConfig, 26 | output: { 27 | file: "dist/esm/index.mjs", 28 | exports: "named", 29 | format: "esm", 30 | }, 31 | }, 32 | { 33 | ...commonConfig, 34 | output: { 35 | file: "dist/esm/index-min.mjs", 36 | exports: "named", 37 | format: "esm", 38 | }, 39 | plugins: [...commonConfig.plugins, terser()], 40 | }, 41 | { 42 | ...commonConfig, 43 | output: { 44 | file: "dist/cjs/index.cjs", 45 | exports: "named", 46 | format: "cjs", 47 | }, 48 | }, 49 | { 50 | ...commonConfig, 51 | output: { 52 | file: "dist/cjs/index-min.cjs", 53 | exports: "named", 54 | format: "cjs", 55 | }, 56 | plugins: [...commonConfig.plugins, terser()], 57 | }, 58 | ]; 59 | export default config; 60 | -------------------------------------------------------------------------------- /packages/router/src/components/KeepAliveRouteOutlet/index.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentType, Fragment, ReactNode, useMemo } from "react"; 2 | import { useLocation, useOutlet } from "react-router"; 3 | import KeepAlive, { KeepAliveProps } from "keepalive-for-react"; 4 | 5 | export interface KeepAliveRouteOutletProps extends Omit { 6 | wrapperComponent?: ComponentType<{ children: ReactNode }>; 7 | activeCacheKey?: string; 8 | } 9 | 10 | function KeepAliveRouteOutlet(props: KeepAliveRouteOutletProps) { 11 | const { wrapperComponent, activeCacheKey: propsActiveCacheKey, ...rest } = props; 12 | const location = useLocation(); 13 | const outlet = useOutlet(); 14 | const WrapperComponent = wrapperComponent || Fragment; 15 | 16 | const activeCacheKey = useMemo(() => { 17 | return propsActiveCacheKey || location.pathname + location.search; 18 | }, [location.pathname, location.search, propsActiveCacheKey]); 19 | 20 | return ( 21 | 22 | {outlet} 23 | 24 | ); 25 | } 26 | 27 | export default KeepAliveRouteOutlet; 28 | -------------------------------------------------------------------------------- /packages/router/src/index.ts: -------------------------------------------------------------------------------- 1 | import KeepAliveRouteOutlet from "./components/KeepAliveRouteOutlet"; 2 | 3 | export { KeepAliveRouteOutlet as default }; 4 | -------------------------------------------------------------------------------- /packages/router/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "jsx": "react-jsx", 13 | /* Linting */ 14 | "strict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "allowSyntheticDefaultImports": true, 19 | "types": ["node"] 20 | }, 21 | "include": ["src/**/*"], 22 | "exclude": ["node_modules", "dist"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/router/tsconfig.types.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "es2015", 5 | "removeComments": false, 6 | "declaration": true, 7 | "declarationMap": false, 8 | "declarationDir": "./dist/types", 9 | "emitDeclarationOnly": true, 10 | "rootDir": "./src" 11 | }, 12 | "exclude": ["src/**/test.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/router/tsconfig.types.tsbuildinfo: -------------------------------------------------------------------------------- 1 | {"root":["./src/index.ts","./src/components/keepaliverouteoutlet/index.tsx"],"errors":true,"version":"5.7.2"} -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | -------------------------------------------------------------------------------- /publish.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { execSync } from "child_process"; 4 | import readline from "readline"; 5 | import fs from "fs"; 6 | import path from "path"; 7 | 8 | const rl = readline.createInterface({ 9 | input: process.stdin, 10 | output: process.stdout, 11 | }); 12 | 13 | const packages = ["core", "router"]; 14 | 15 | function getPackageVersion(pkgName) { 16 | const pkgPath = path.join(process.cwd(), "packages", pkgName, "package.json"); 17 | const pkgJson = JSON.parse(fs.readFileSync(pkgPath, "utf8")); 18 | return pkgJson.version; 19 | } 20 | 21 | function updateVersion(pkgName, versionType) { 22 | console.log(`\nUpdating ${pkgName} version...`); 23 | try { 24 | execSync(`pnpm version:${pkgName} ${versionType}`, { stdio: "inherit" }); 25 | return true; 26 | } catch (error) { 27 | console.error(`Error updating version for ${pkgName}:`, error.message); 28 | return false; 29 | } 30 | } 31 | 32 | function buildPackages() { 33 | console.log("\nBuilding all packages..."); 34 | try { 35 | execSync("pnpm build", { stdio: "inherit" }); 36 | return true; 37 | } catch (error) { 38 | console.error("Error building packages:", error.message); 39 | return false; 40 | } 41 | } 42 | 43 | function publishPackage(pkgName) { 44 | console.log(`\nPublishing ${pkgName}...`); 45 | try { 46 | execSync(`pnpm publish:${pkgName}`, { stdio: "inherit" }); 47 | return true; 48 | } catch (error) { 49 | console.error(`Error publishing ${pkgName}:`, error.message); 50 | return false; 51 | } 52 | } 53 | 54 | function displayCurrentVersions() { 55 | console.log("\nCurrent package versions:"); 56 | for (const pkg of packages) { 57 | console.log(`- ${pkg}: v${getPackageVersion(pkg)}`); 58 | } 59 | } 60 | 61 | async function promptUser(question) { 62 | return new Promise(resolve => { 63 | rl.question(question, answer => { 64 | resolve(answer.trim()); 65 | }); 66 | }); 67 | } 68 | 69 | async function main() { 70 | console.log("=== keepalive-for-react Package Publisher ==="); 71 | displayCurrentVersions(); 72 | 73 | const publishAll = await promptUser("\nDo you want to publish all packages? (y/n): "); 74 | 75 | if (publishAll.toLowerCase() === "y") { 76 | const versionType = await promptUser("Version update type (patch/minor/major): "); 77 | 78 | if (!["patch", "minor", "major"].includes(versionType)) { 79 | console.error("Invalid version type. Use patch, minor, or major."); 80 | rl.close(); 81 | return; 82 | } 83 | 84 | for (const pkg of packages) { 85 | updateVersion(pkg, versionType); 86 | } 87 | 88 | if (!buildPackages()) { 89 | rl.close(); 90 | return; 91 | } 92 | 93 | const confirmPublish = await promptUser("Ready to publish? This will publish to npm registry (y/n): "); 94 | 95 | if (confirmPublish.toLowerCase() === "y") { 96 | for (const pkg of packages) { 97 | publishPackage(pkg); 98 | } 99 | console.log("\n✅ All packages published successfully!"); 100 | } else { 101 | console.log("\nPublish canceled."); 102 | } 103 | } else { 104 | const packagesToPublish = []; 105 | 106 | for (const pkg of packages) { 107 | const shouldPublish = await promptUser(`Publish ${pkg}? (y/n): `); 108 | if (shouldPublish.toLowerCase() === "y") { 109 | packagesToPublish.push(pkg); 110 | 111 | const versionType = await promptUser(`Version update for ${pkg} (patch/minor/major): `); 112 | if (!["patch", "minor", "major"].includes(versionType)) { 113 | console.error("Invalid version type. Using patch as default."); 114 | updateVersion(pkg, "patch"); 115 | } else { 116 | updateVersion(pkg, versionType); 117 | } 118 | } 119 | } 120 | 121 | if (packagesToPublish.length === 0) { 122 | console.log("No packages selected for publishing."); 123 | rl.close(); 124 | return; 125 | } 126 | 127 | if (!buildPackages()) { 128 | rl.close(); 129 | return; 130 | } 131 | 132 | const confirmPublish = await promptUser("Ready to publish selected packages? (y/n): "); 133 | 134 | if (confirmPublish.toLowerCase() === "y") { 135 | for (const pkg of packagesToPublish) { 136 | publishPackage(pkg); 137 | } 138 | console.log("\n✅ Selected packages published successfully!"); 139 | } else { 140 | console.log("\nPublish canceled."); 141 | } 142 | } 143 | 144 | rl.close(); 145 | } 146 | 147 | main().catch(error => { 148 | console.error("Error:", error); 149 | rl.close(); 150 | }); 151 | -------------------------------------------------------------------------------- /react-keepalive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/irychen/keepalive-for-react/bde1c3feb4d53e54b8840ecf85064179757bdb64/react-keepalive.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "jsx": "react-jsx", 13 | /* Linting */ 14 | "strict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "allowSyntheticDefaultImports": true, 19 | "types": ["node"] 20 | }, 21 | "exclude": ["node_modules", "dist"] 22 | } 23 | --------------------------------------------------------------------------------