├── .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 |
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 | [](https://npmjs.com/package/keepalive-for-react) [](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) | [](https://npmjs.com/package/keepalive-for-react) | Core keepalive functionality |
22 | | [keepalive-for-react-router](./packages/router) | [](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 | [](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 | [](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 |
3 |
4 |
5 |
6 |
7 | React KeepAlive 组件
8 |
9 |
10 |
11 | 一个类似Vue中keep-alive的React KeepAlive组件
12 |
13 | [English](./README.md) | 中文
14 |
15 | [](https://npmjs.com/package/keepalive-for-react) [](https://npmjs.com/package/keepalive-for-react)
16 |
17 | ## 包信息
18 |
19 | | 包名 | 版本 | 描述 |
20 | | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- |
21 | | [keepalive-for-react](./packages/core) | [](https://npmjs.com/package/keepalive-for-react) | 核心keepalive功能 |
22 | | [keepalive-for-react-router](./packages/router) | [](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 | [](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 | [](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