—— 适配 React 18,多页签缓存,解决 useRef 丢失问题
🧠 为什么要从 react-activation 迁移?
React18 在 StrictMode 下会:
double-render(mount → unmount → mount)
Offscreen 回收隐藏组件
导致 KeepAlive 中的组件被卸载
useRef/state 直接丢失
👉 react-activation 已经不维护,不兼容 React18 + Umi4。
而:
✔ react-keepalive-router 完全兼容 React18
✔ 专为 react-router v6 & 后台系统的多 Tab 构建
✔ 不依赖 Offscreen hack
✔ useRef 永不丢失
🧩 迁移步骤
① 卸载旧包
npm remove react-activation
② 安装新包
npm install react-keepalive-router
③ 替换组件引用
- import { AliveScope, KeepAlive } from 'react-activation';
+ import { KeepAliveProvider, KeepAlive } from 'react-keepalive-router';
🔥 【最终版】AntD Pro 的 KeepAliveLayout(Markdown 完整可复制)
import { history, useIntl, useModel } from '@umijs/max';
import { Dropdown, Tabs } from 'antd';
import { useCallback, useMemo } from 'react';
import { CloseOutlined } from '@ant-design/icons';
import type { ItemType, MenuInfo } from 'rc-menu/lib/interface';
import {
KeepAliveProvider,
KeepAlive,
} from 'react-keepalive-router';
import { KeepAliveTabContext } from './context';
import { KeepAliveTab, useKeepAliveTabs } from './useKeepAliveTabs';
enum OperationType {
REFRESH = 'refresh',
CLOSE = 'close',
CLOSEOTHER = 'close-other',
}
type MenuItemType = (ItemType & { key: OperationType }) | null;
const KeepAliveLayout = () => {
const {
keepAliveTabs,
activeTabRoutePath,
closeTab,
refreshTab,
closeOtherTab,
onHidden,
onShow,
getTabDom,
ROUTE_TAB_CONTENT_PREFIX,
} = useKeepAliveTabs();
const { initialState } = useModel('@@initialState');
const intl = useIntl();
const menuItems: MenuItemType[] = useMemo(
() =>
[
{
label: intl.formatMessage({
id: 'component.layout.route.tabs.operation.refreshTab',
defaultMessage: '刷新',
}),
key: OperationType.REFRESH,
},
keepAliveTabs.length <= 1
? null
: {
label: intl.formatMessage({
id: 'component.layout.route.tabs.operation.closeTab',
defaultMessage: '关闭',
}),
key: OperationType.CLOSE,
},
keepAliveTabs.length <= 1
? null
: {
label: intl.formatMessage({
id: 'component.layout.route.tabs.operation.closeOtherTab',
defaultMessage: '关闭其他',
}),
key: OperationType.CLOSEOTHER,
},
].filter((o) => o),
[keepAliveTabs],
);
const menuClick = useCallback(
({ key, domEvent }: MenuInfo, tab: KeepAliveTab) => {
domEvent.stopPropagation();
if (key === OperationType.REFRESH) {
refreshTab(tab.routePath);
} else if (key === OperationType.CLOSE) {
closeTab(tab.routePath);
} else if (key === OperationType.CLOSEOTHER) {
closeOtherTab(tab.routePath);
}
},
[closeOtherTab, closeTab, refreshTab],
);
const renderTabTitle = useCallback(
(tab: KeepAliveTab) => {
const menuMap = initialState?.permissionMenuMap;
const menuInfo = menuMap?.get(tab.routePath);
return (
<Dropdown
menu={{ items: menuItems, onClick: (e) => menuClick(e, tab) }}
trigger={['contextMenu']}
>
<div style={{ margin: '-12px 0', padding: '12px 0' }}>
{menuInfo && menuInfo.attr7 ? (
<i className={menuInfo.attr7} style={{ marginRight: 4 }} />
) : null}
{tab.title}
</div>
</Dropdown>
);
},
[menuItems],
);
const tabItems = useMemo(() => {
return keepAliveTabs.map((tab, tabIndex) => ({
key: tab.routePath,
label: renderTabTitle(tab),
children: (
<KeepAlive
id={tab.routePath}
name={tab.routePath}
saveScroll
>
<div
key={tab.key}
id={ROUTE_TAB_CONTENT_PREFIX + tab.pathname}
style={{
height: 'calc(100vh - 96px)',
overflow: 'auto',
padding: 24,
}}
>
{tab.children}
</div>
</KeepAlive>
),
closeIcon: (
<CloseOutlined className="keep-alive-tabs-close-icon" />
),
closable: tabIndex > 0,
}));
}, [keepAliveTabs]);
const onTabsChange = useCallback(
(tabRoutePath: string) => {
const curTab = keepAliveTabs.find((o) => o.routePath === tabRoutePath);
if (curTab) {
history.push(curTab.pathname);
}
},
[keepAliveTabs],
);
const onTabEdit = (
targetKey: React.MouseEvent | React.KeyboardEvent | string,
action: 'add' | 'remove',
) => {
if (action === 'remove') {
closeTab(targetKey as string);
}
};
const keepAliveContextValue = useMemo(
() => ({
closeTab,
closeOtherTab,
refreshTab,
onHidden,
onShow,
getTabDom,
}),
[closeTab, closeOtherTab, refreshTab, onHidden, onShow, getTabDom],
);
return (
<KeepAliveProvider>
<KeepAliveTabContext.Provider value={keepAliveContextValue}>
<Tabs
type="editable-card"
items={tabItems}
activeKey={activeTabRoutePath}
onChange={onTabsChange}
className="keep-alive-tabs"
hideAdd
animated={false}
onEdit={onTabEdit}
destroyInactiveTabPane
/>
</KeepAliveTabContext.Provider>
</KeepAliveProvider>
);
};
export default KeepAliveLayout;