Repository: irychen/keepalive-for-react
Branch: main
Commit: 39671211ed31
Files: 94
Total size: 121.0 KB
Directory structure:
gitextract_xi2rhxav/
├── .changeset/
│ ├── README.md
│ └── config.json
├── .claude/
│ └── settings.json
├── .github/
│ └── ISSUE_TEMPLATE/
│ ├── bug_report.md
│ ├── custom.md
│ └── feature_request.md
├── .gitignore
├── .husky/
│ └── pre-commit
├── .prettierrc
├── CLAUDE.md
├── LICENSE
├── README.md
├── README.zh_CN.md
├── examples/
│ ├── react-router-dom-simple-starter/
│ │ ├── .gitignore
│ │ ├── .npmrc
│ │ ├── README.md
│ │ ├── eslint.config.js
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── postcss.config.js
│ │ ├── src/
│ │ │ ├── App.tsx
│ │ │ ├── index.css
│ │ │ ├── layout/
│ │ │ │ └── index.tsx
│ │ │ ├── main.tsx
│ │ │ ├── pages/
│ │ │ │ ├── about/
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── counter/
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── home/
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── nested/
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── nested-a/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ └── nested-b/
│ │ │ │ │ └── index.tsx
│ │ │ │ └── nocache-counter/
│ │ │ │ └── index.tsx
│ │ │ ├── router/
│ │ │ │ └── index.tsx
│ │ │ └── vite-env.d.ts
│ │ ├── tailwind.config.js
│ │ ├── tsconfig.app.json
│ │ ├── tsconfig.app.tsbuildinfo
│ │ ├── tsconfig.json
│ │ ├── tsconfig.node.json
│ │ ├── tsconfig.node.tsbuildinfo
│ │ └── vite.config.ts
│ └── simple-tabs-starter/
│ ├── .gitignore
│ ├── .npmrc
│ ├── README.md
│ ├── eslint.config.js
│ ├── index.html
│ ├── package.json
│ ├── pnpm-workspace.yaml
│ ├── postcss.config.js
│ ├── src/
│ │ ├── App.tsx
│ │ ├── index.css
│ │ ├── main.tsx
│ │ ├── store/
│ │ │ └── counter-store.tsx
│ │ └── vite-env.d.ts
│ ├── tailwind.config.js
│ ├── tsconfig.app.json
│ ├── tsconfig.app.tsbuildinfo
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ ├── tsconfig.node.tsbuildinfo
│ └── vite.config.ts
├── package.json
├── packages/
│ ├── core/
│ │ ├── CHANGELOG.md
│ │ ├── README.md
│ │ ├── README.zh_CN.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── compat/
│ │ │ │ ├── Activity.tsx
│ │ │ │ └── safeStartTransition.ts
│ │ │ ├── components/
│ │ │ │ ├── CacheComponent/
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── CacheComponentProvider/
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── CacheContext/
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── KeepAlive/
│ │ │ │ │ └── index.tsx
│ │ │ │ └── MemoizedActivty/
│ │ │ │ └── index.tsx
│ │ │ ├── event/
│ │ │ │ └── index.ts
│ │ │ ├── hooks/
│ │ │ │ ├── onDestory.ts
│ │ │ │ ├── useEffectOnActive.ts
│ │ │ │ ├── useEffectOnCreate.ts
│ │ │ │ ├── useKeepAliveContext.ts
│ │ │ │ ├── useLayoutEffectOnActive.ts
│ │ │ │ ├── useLayoutEffectOnCreate.ts
│ │ │ │ ├── useOnActive.ts
│ │ │ │ └── useOnCreate.ts
│ │ │ ├── index.ts
│ │ │ └── utils/
│ │ │ └── index.tsx
│ │ ├── tsconfig.json
│ │ └── tsup.config.ts
│ └── router/
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── package.json
│ ├── src/
│ │ ├── components/
│ │ │ └── KeepAliveRouteOutlet/
│ │ │ └── index.tsx
│ │ └── index.ts
│ ├── tsconfig.json
│ └── tsup.config.ts
├── pnpm-workspace.yaml
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .changeset/README.md
================================================
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
================================================
FILE: .changeset/config.json
================================================
{
"$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [["keepalive-for-react", "keepalive-for-react-router"]],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
================================================
FILE: .claude/settings.json
================================================
{
"permissions": {
"defaultMode": "bypassPermissions"
},
"includeCoAuthoredBy": false,
"attribution": {
"commit": "",
"pr": ""
},
"env": {
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"
}
}
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/custom.md
================================================
---
name: Custom issue template
about: Describe this issue template's purpose here.
title: ''
labels: ''
assignees: ''
---
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .gitignore
================================================
node_modules
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
dist
================================================
FILE: .husky/pre-commit
================================================
pnpm exec lint-staged
================================================
FILE: .prettierrc
================================================
{
"printWidth": 140,
"tabWidth": 4,
"useTabs": false,
"singleQuote": false,
"jsxSingleQuote": false,
"semi": true,
"trailingComma": "all",
"bracketSpacing": true,
"arrowParens": "avoid"
}
================================================
FILE: CLAUDE.md
================================================
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Repository layout
pnpm workspace (see `pnpm-workspace.yaml`). Two publishable packages, plus two runnable examples:
- `packages/core` → `keepalive-for-react` — the `` component and hooks. Only runtime dep is `mitt`.
- `packages/router` → `keepalive-for-react-router` — thin `` wrapper that reads `react-router`'s `useLocation`/`useOutlet` and feeds them into core. Peer-depends on `keepalive-for-react` and `react-router >= 6`.
- `examples/react-router-dom-simple-starter`, `examples/simple-tabs-starter` — Vite + React playgrounds used to smoke-test changes.
Versions of the two packages are **linked** in `.changeset/config.json` — they always bump together, so a change in core should almost always have a matching changeset for router (even if router's code didn't change).
## Commands
All run from repo root unless noted:
```bash
pnpm install # install workspace
pnpm build # tsup build for every package (pnpm -r build)
pnpm clean # rm -rf dist in every package
pnpm format # prettier --write on ts/tsx/json
pnpm changeset # author a changeset
pnpm version # apply changesets (bump versions + changelogs)
pnpm release # build + changeset publish
pnpm example:router # run the react-router example
pnpm example:tabs # run the simple-tabs example
```
Per-package build: `pnpm --filter keepalive-for-react build` (or `...-router`). Both use `tsup` with `src/index.ts` as entry, emitting `cjs`+`esm`+`.d.ts`, with `react`/`react-dom`/`react/jsx-runtime` external. There is **no test suite** — verify changes by running the examples.
Pre-commit: husky runs `lint-staged` which runs prettier on staged files.
## Architecture
### Core runtime model (`packages/core/src/components/KeepAlive/index.tsx`)
`KeepAlive` holds a `cacheNodes: CacheNode[]` state (`{ cacheKey, ele, lastActiveTime, renderCount }`). On every `activeCacheKey`/`children` change (inside `useLayoutEffect` + `safeStartTransition`):
- If a node for the key exists → update its `ele` and `lastActiveTime`. If `maxAliveTime` has elapsed (global number or per-key `MaxAliveConfig[]`), bump `renderCount` (forces a remount) and emit `destroy` so `onCreate` cleanups fire.
- If not → push a new node. When length exceeds `max`, the LRU node (lowest `lastActiveTime`) is evicted and `destroy` is emitted for its key.
`refresh`/`destroy`/`destroyAll`/`destroyOther` all emit via the `mitt` event bus (`src/event/index.ts`) before mutating state — this is the _only_ way create-time cleanups (`useEffectOnCreate`) are notified, so **any code path that removes or remounts a cache node must emit the right event first**. See commit `6c73bd1` for the precedent.
`aliveRef` exposes this API imperatively via `useImperativeHandle`; `useKeepAliveRef()` is just `useRef(null)`.
### DOM hand-off (`packages/core/src/components/CacheComponent/index.tsx`)
Each cache node renders into its own imperatively-created `
` via `createPortal`. `KeepAlive` renders one `containerDivRef` element; `CacheComponent` uses a `useLayoutEffect` to move the currently-active cache div into that container and toggle `.active`/`.inactive` classes on siblings.
Three swap modes, picked from props:
- **`transition`**: mark prev siblings `.inactive`, wait `duration - 40ms`, remove them, then append the new div. Relies on user CSS keying off `.active`/`.inactive`.
- **`viewTransition`**: wraps the sync swap in `document.startViewTransition(...)`.
- **default**: synchronous swap inside the same `useLayoutEffect`.
When inactive **and** not in `include` / matching `exclude`, `CacheComponent` calls `destroy(cacheKey)` itself — this is how `include`/`exclude` eviction flows back into the parent's state.
A cache node only renders its children once `activatedRef.current` has ever been true, so cached-but-never-visited children are not mounted.
### Context + hooks
`CacheComponentProvider` wraps each cache node's children in `CacheComponentContext`, exposing `{ active, refresh, destroy, destroyAll, destroyOther, getCacheNodes, _cacheKey }`. `_cacheKey` is the private hook used by create-time lifecycle hooks to match `destroy` events.
- `useEffectOnActive` / `useLayoutEffectOnActive` — via `useOnActive`, skip when `!active`, optional `skipMount` to no-op the first render.
- `useEffectOnCreate` / `useLayoutEffectOnCreate` — via `useOnCreate`, run once, store the returned cleanup, then subscribe to the event bus so cleanup fires on `destroy`/`destroyAll`/`destroyOther`/`refresh` for the matching `_cacheKey`.
- `useKeepAliveContext` — direct read of the context.
### React version compatibility (`packages/core/src/compat/`)
- `Activity.tsx` feature-detects `React.Activity` (19.2+) and sets `hasNativeActivity`; `MemoizedActivty` delays the `visible`→`hidden` flip by `duration` ms so native Activity plays nicely with the transition swap.
- `safeStartTransition.ts` falls back to a synchronous call when `startTransition` isn't defined (< React 18).
- `enableActivity` is off by default. Turning it on changes the semantics of `useEffect` inside children: effects re-run on every activation instead of once at mount.
Peer-deps allow React `>=16.8.0`. README pins the split: use `keepalive-for-react@4.x` for React 18, `@5.x` for React 19.2+. React's `StrictMode` is known-incompatible in dev and must not be used.
### Router adapter (`packages/router/src/components/KeepAliveRouteOutlet/index.tsx`)
Tiny component: `activeCacheKey` defaults to `location.pathname + location.search`, and `children` is `useOutlet()` (optionally wrapped in `wrapperComponent`). All other `KeepAliveProps` are forwarded. If you need anything router-specific, add it here — core has no router awareness.
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2024 Rychen Wong
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
KeepAlive for React
A React KeepAlive component like keep-alive in vue
[中文](./README.zh_CN.md) | English
[](https://npmjs.com/package/keepalive-for-react) [](https://npmjs.com/package/keepalive-for-react) [![][discord-shield]][discord-link]
## Packages
| Package | Version | Description |
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
| [keepalive-for-react](./packages/core) | [](https://npmjs.com/package/keepalive-for-react) | Core keepalive functionality |
| [keepalive-for-react-router](./packages/router) | [](https://npmjs.com/package/keepalive-for-react-router) | React Router integration |
## Features
- Support react-router-dom v6+ or react-router v7+
- Support React v16+ ~ v18+ (v19.2 Activity component support [v5.0.0])
- Support Suspense and Lazy import
- Support ErrorBoundary
- Support Custom Container
- Support Switching Animation Transition with className `active` and `inactive`
- Simply implement, without any extra dependencies and hacking ways
- Only 6KB minified size
- Support interrupt state effect when component is not active (v5.0.0)
## Attention
- **Version Compatibility**:
- For React 18, please use `keepalive-for-react@4.x.x`
- For React 19.2+, please use `keepalive-for-react@5.x.x`
- DO NOT use , it CANNOT work with keepalive-for-react in development mode. because it can lead to
some unexpected behavior.
- In Router only support react-router-dom v6+
## Install
```bash
npm install keepalive-for-react
```
```bash
yarn add keepalive-for-react
```
```bash
pnpm add keepalive-for-react
```
## Usage
### in react-router-dom v6+ or react-router v7+
1. install react-router-dom v6+ or react-router v7+
```bash
# v6+
npm install react-router-dom keepalive-for-react keepalive-for-react-router@1.x.x
# v7+
npm install react-router keepalive-for-react keepalive-for-react-router@2.x.x
```
2. use KeepAlive in your project
```tsx
// v6+ keepalive-for-react-router@1.x.x
// v7+ keepalive-for-react-router@2.x.x
import KeepAliveRouteOutlet from "keepalive-for-react-router";
function Layout() {
return (
);
}
```
or
```tsx
import { useMemo } from "react";
// v6+
import { useLocation, useOutlet } from "react-router-dom";
// v7
// import { useLocation, useOutlet } from "react-router";
import { KeepAlive, useKeepAliveRef } from "keepalive-for-react";
function Layout() {
const location = useLocation();
const aliveRef = useKeepAliveRef();
const outlet = useOutlet();
// determine which route component to is active
const currentCacheKey = useMemo(() => {
return location.pathname + location.search;
}, [location.pathname, location.search]);
return (
);
}
```
details see [examples/simple-tabs-starter](./examples/simple-tabs-starter)
[](https://stackblitz.com/github/finedaybreak/keepalive-for-react/tree/main/examples/simple-tabs-starter)
## KeepAlive Props
type definition
```tsx
interface KeepAliveProps {
// determine which component to is active
activeCacheKey: string;
children?: KeepAliveChildren;
/**
* max cache count default 10
*/
max?: number;
exclude?: Array | string | RegExp;
include?: Array | string | RegExp;
onBeforeActive?: (activeCacheKey: string) => void;
customContainerRef?: RefObject;
cacheNodeClassName?: string;
containerClassName?: string;
errorElement?: ComponentType<{
children: ReactNode;
}>;
/**
* transition default false
*/
transition?: boolean;
/**
* use view transition to animate the component when switching tabs
* @see https://developer.chrome.com/docs/web-platform/view-transitions/
*/
viewTransition?: boolean;
/**
* transition duration default 200
*/
duration?: number;
aliveRef?: RefObject;
/**
* max alive time for cache node (second)
* @default 0 (no limit)
*/
maxAliveTime?: number | MaxAliveConfig[];
/**
* enable Activity component from react 19.2+
* @default false
* Attention: if enable Activity component, useEffect will trigger when the component is active
*/
enableActivity?: boolean;
}
interface MaxAliveConfig {
match: string | RegExp;
expire: number;
}
```
## Hooks
### useEffectOnActive
```tsx
useEffectOnActive(() => {
console.log("active");
}, []);
```
### useLayoutEffectOnActive
```tsx
useLayoutEffectOnActive(
() => {
console.log("active");
},
[],
false,
);
// the third parameter is optional, default is false,
// if true, which means the callback will be skipped when the useLayoutEffect is triggered in first render
```
### useEffectOnCreate
Run a callback only once when the component is first created (cached), and run the returned cleanup only when the component is destroyed from the cache. Unlike `useEffect(fn, [])`, it will NOT re-run when the cached component is re-activated.
```tsx
useEffectOnCreate(() => {
console.log("component created");
return () => {
console.log("component destroyed");
};
});
```
### useLayoutEffectOnCreate
Same as `useEffectOnCreate` but uses `useLayoutEffect` internally. Useful when the create-time logic needs to run synchronously before the browser paints.
```tsx
useLayoutEffectOnCreate(() => {
console.log("component created (layout)");
return () => {
console.log("component destroyed (layout)");
};
});
```
### useKeepAliveContext
type definition
```ts
interface KeepAliveContext {
/**
* whether the component is active
*/
active: boolean;
/**
* refresh the component
* @param {string} [cacheKey] - The cache key of the component. If not provided, the current cached component will be refreshed.
*/
refresh: (cacheKey?: string) => void;
/**
* destroy the component
* @param {string} [cacheKey] - the cache key of the component, if not provided, current active cached component will be destroyed
*/
destroy: (cacheKey?: string | string[]) => Promise;
/**
* destroy all components
*/
destroyAll: () => Promise;
/**
* destroy other components except the provided cacheKey
* @param {string} [cacheKey] - The cache key of the component. If not provided, destroy all components except the current active cached component.
*/
destroyOther: (cacheKey?: string) => Promise;
/**
* get the cache nodes
*/
getCacheNodes: () => Array;
}
```
```tsx
const { active, refresh, destroy, getCacheNodes } = useKeepAliveContext();
// active is a boolean, true is active, false is inactive
// refresh is a function, you can call it to refresh the component
// destroy is a function, you can call it to destroy the component
// ...
// getCacheNodes is a function, you can call it to get the cache nodes
```
### useKeepAliveRef
type definition
```ts
interface KeepAliveRef {
refresh: (cacheKey?: string) => void;
destroy: (cacheKey?: string | string[]) => Promise;
destroyAll: () => Promise;
destroyOther: (cacheKey?: string) => Promise;
getCacheNodes: () => Array;
}
```
```tsx
function App() {
const aliveRef = useKeepAliveRef();
// aliveRef.current is a KeepAliveRef object
// you can call refresh and destroy on aliveRef.current
aliveRef.current?.refresh();
// it is not necessary to call destroy manually, KeepAlive will handle it automatically
aliveRef.current?.destroy();
return {/* ... */};
}
// or
function AppRouter() {
const aliveRef = useKeepAliveRef();
// aliveRef.current is a KeepAliveRef object
// you can call refresh and destroy on aliveRef.current
aliveRef.current?.refresh();
aliveRef.current?.destroy();
return ;
}
```
## Development
install dependencies
```bash
pnpm install
```
build package
```bash
pnpm build
```
[discord-link]: https://discord.gg/ycf896w7eA
[discord-shield]: https://img.shields.io/discord/1232158668913381467?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square
[discord-shield-badge]: https://img.shields.io/discord/1232158668913381467?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=for-the-badge
================================================
FILE: README.zh_CN.md
================================================
);
}
export default About;
================================================
FILE: examples/react-router-dom-simple-starter/src/pages/counter/index.tsx
================================================
import { useEffectOnActive, useEffectOnCreate, useKeepAliveContext } from "keepalive-for-react";
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
const { refresh, active } = useKeepAliveContext();
// useEffectOnActive(() => {
// console.log("Counter is active (OnActive)", count);
// return () => {
// console.log("Counter is destroyed (OnActive)", count);
// };
// }, [count]);
useEffectOnCreate(() => {
console.log("Counter is created (OnCreate)", count);
return () => {
console.log("Counter is destroyed (_OnCreate)", count);
};
});
return (
Counter
Active: {active ? "true" : "false"}
{count}
);
}
export default Counter;
================================================
FILE: examples/react-router-dom-simple-starter/src/pages/home/index.tsx
================================================
import { useEffectOnActive } from "keepalive-for-react";
import { useRef } from "react";
function Home() {
const domRef = useRef(null);
useEffectOnActive(
() => {
console.log("Home is active");
const dom = domRef.current;
if (dom) {
// if transition is true, the dom size will be 0, because the dom is not rendered yet
console.log(`home dom size: height ${dom.clientHeight}px width ${dom.clientWidth}px`);
setTimeout(() => {
console.log(`home dom size: height ${dom.clientHeight}px width ${dom.clientWidth}px`);
}, 300);
}
},
[],
true,
);
return (
Home
Welcome to the home page, this is a simple example of how to use keepalive-for-react with react-router-dom.
A React KeepAlive component like keep-alive in vue
[中文](./README.zh_CN.md) | English
[](https://npmjs.com/package/keepalive-for-react) [](https://npmjs.com/package/keepalive-for-react) [![][discord-shield]][discord-link]
## Packages
| Package | Version | Description |
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
| [keepalive-for-react](./packages/core) | [](https://npmjs.com/package/keepalive-for-react) | Core keepalive functionality |
| [keepalive-for-react-router](./packages/router) | [](https://npmjs.com/package/keepalive-for-react-router) | React Router integration |
## Features
- Support react-router-dom v6+ or react-router v7+
- Support React v16+ ~ v18+ (v19.2 Activity component support [v5.0.0])
- Support Suspense and Lazy import
- Support ErrorBoundary
- Support Custom Container
- Support Switching Animation Transition with className `active` and `inactive`
- Simply implement, without any extra dependencies and hacking ways
- Only 6KB minified size
- Support interrupt state effect when component is not active (v5.0.0)
## Attention
- **Version Compatibility**:
- For React 18, please use `keepalive-for-react@4.x.x`
- For React 19.2+, please use `keepalive-for-react@5.x.x`
- DO NOT use , it CANNOT work with keepalive-for-react in development mode. because it can lead to
some unexpected behavior.
- In Router only support react-router-dom v6+
## Install
```bash
npm install keepalive-for-react
```
```bash
yarn add keepalive-for-react
```
```bash
pnpm add keepalive-for-react
```
## Usage
### in react-router-dom v6+ or react-router v7+
1. install react-router-dom v6+ or react-router v7+
```bash
# v6+
npm install react-router-dom keepalive-for-react keepalive-for-react-router@1.x.x
# v7+
npm install react-router keepalive-for-react keepalive-for-react-router@2.x.x
```
2. use KeepAlive in your project
```tsx
// v6+ keepalive-for-react-router@1.x.x
// v7+ keepalive-for-react-router@2.x.x
import KeepAliveRouteOutlet from "keepalive-for-react-router";
function Layout() {
return (
);
}
```
or
```tsx
import { useMemo } from "react";
// v6+
import { useLocation, useOutlet } from "react-router-dom";
// v7
// import { useLocation, useOutlet } from "react-router";
import { KeepAlive, useKeepAliveRef } from "keepalive-for-react";
function Layout() {
const location = useLocation();
const aliveRef = useKeepAliveRef();
const outlet = useOutlet();
// determine which route component to is active
const currentCacheKey = useMemo(() => {
return location.pathname + location.search;
}, [location.pathname, location.search]);
return (
);
}
```
details see [examples/simple-tabs-starter](./examples/simple-tabs-starter)
[](https://stackblitz.com/github/finedaybreak/keepalive-for-react/tree/main/examples/simple-tabs-starter)
## KeepAlive Props
type definition
```tsx
interface KeepAliveProps {
// determine which component to is active
activeCacheKey: string;
children?: KeepAliveChildren;
/**
* max cache count default 10
*/
max?: number;
exclude?: Array | string | RegExp;
include?: Array | string | RegExp;
onBeforeActive?: (activeCacheKey: string) => void;
customContainerRef?: RefObject;
cacheNodeClassName?: string;
containerClassName?: string;
errorElement?: ComponentType<{
children: ReactNode;
}>;
/**
* transition default false
*/
transition?: boolean;
/**
* use view transition to animate the component when switching tabs
* @see https://developer.chrome.com/docs/web-platform/view-transitions/
*/
viewTransition?: boolean;
/**
* transition duration default 200
*/
duration?: number;
aliveRef?: RefObject;
/**
* max alive time for cache node (second)
* @default 0 (no limit)
*/
maxAliveTime?: number | MaxAliveConfig[];
/**
* enable Activity component from react 19+
* @default false
* Activity component can improve performance
* Attention: if enable Activity component, useEffect will trigger when the component is active
*/
enableActivity?: boolean;
}
interface MaxAliveConfig {
match: string | RegExp;
expire: number;
}
```
## Hooks
### useEffectOnActive
```tsx
useEffectOnActive(() => {
console.log("active");
}, []);
```
### useLayoutEffectOnActive
```tsx
useLayoutEffectOnActive(
() => {
console.log("active");
},
[],
false,
);
// the third parameter is optional, default is false,
// if true, which means the callback will be skipped when the useLayoutEffect is triggered in first render
```
### useEffectOnCreate
Run a callback only once when the component is first created (cached), and run the returned cleanup only when the component is destroyed from the cache. Unlike `useEffect(fn, [])`, it will NOT re-run when the cached component is re-activated.
```tsx
useEffectOnCreate(() => {
console.log("component created");
return () => {
console.log("component destroyed");
};
});
```
### useLayoutEffectOnCreate
Same as `useEffectOnCreate` but uses `useLayoutEffect` internally. Useful when the create-time logic needs to run synchronously before the browser paints.
```tsx
useLayoutEffectOnCreate(() => {
console.log("component created (layout)");
return () => {
console.log("component destroyed (layout)");
};
});
```
### useKeepAliveContext
type definition
```ts
interface KeepAliveContext {
/**
* whether the component is active
*/
active: boolean;
/**
* refresh the component
* @param {string} [cacheKey] - The cache key of the component. If not provided, the current cached component will be refreshed.
*/
refresh: (cacheKey?: string) => void;
/**
* destroy the component
* @param {string} [cacheKey] - the cache key of the component, if not provided, current active cached component will be destroyed
*/
destroy: (cacheKey?: string | string[]) => Promise;
/**
* destroy all components
*/
destroyAll: () => Promise;
/**
* destroy other components except the provided cacheKey
* @param {string} [cacheKey] - The cache key of the component. If not provided, destroy all components except the current active cached component.
*/
destroyOther: (cacheKey?: string) => Promise;
/**
* get the cache nodes
*/
getCacheNodes: () => Array;
}
```
```tsx
const { active, refresh, destroy, getCacheNodes } = useKeepAliveContext();
// active is a boolean, true is active, false is inactive
// refresh is a function, you can call it to refresh the component
// destroy is a function, you can call it to destroy the component
// ...
// getCacheNodes is a function, you can call it to get the cache nodes
```
### useKeepAliveRef
type definition
```ts
interface KeepAliveRef {
refresh: (cacheKey?: string) => void;
destroy: (cacheKey?: string | string[]) => Promise;
destroyAll: () => Promise;
destroyOther: (cacheKey?: string) => Promise;
getCacheNodes: () => Array;
}
```
```tsx
function App() {
const aliveRef = useKeepAliveRef();
// aliveRef.current is a KeepAliveRef object
// you can call refresh and destroy on aliveRef.current
aliveRef.current?.refresh();
// it is not necessary to call destroy manually, KeepAlive will handle it automatically
aliveRef.current?.destroy();
return {/* ... */};
}
// or
function AppRouter() {
const aliveRef = useKeepAliveRef();
// aliveRef.current is a KeepAliveRef object
// you can call refresh and destroy on aliveRef.current
aliveRef.current?.refresh();
aliveRef.current?.destroy();
return ;
}
```
## Development
install dependencies
```bash
pnpm install
```
build package
```bash
pnpm build
```
[discord-link]: https://discord.gg/ycf896w7eA
[discord-shield]: https://img.shields.io/discord/1232158668913381467?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square
[discord-shield-badge]: https://img.shields.io/discord/1232158668913381467?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=for-the-badge
================================================
FILE: packages/core/README.zh_CN.md
================================================