- 不要使用
<React.StrictMode />
严格模式 - (React v18+) 不要使用
ReactDOMClient.createRoot
, 而是使用ReactDOM.render
, #225 (comment)
English | 中文说明
Vue 中 <keep-alive />
功能在 React 中的黑客实现
建议关注 React 18.x
中的官方实现 <Offscreen />
配合 babel 预编译实现更稳定的 KeepAlive 功能
-
React v16 / v17 / v18
-
Preact v10+
-
兼容 SSR
yarn add react-activation
# 或者
npm install react-activation
该插件将借助 react-node-key
于编译阶段在各 JSX 元素上增加 _nk
属性,帮助 react-activation
在运行时按渲染位置生成唯一的缓存 id 标识
{
"plugins": [
"react-activation/babel"
]
}
(0.11.0+)如果不使用 babel,建议给每个 <KeepAlive>
声明全局唯一且不变的 cacheKey
属性,以确保缓存的稳定性,如下
<KeepAlive cacheKey="UNIQUE_ID" />
如例子中的 <Counter>
组件
// App.js
import React, { useState } from 'react'
import KeepAlive from 'react-activation'
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>count: {count}</p>
<button onClick={() => setCount(count => count + 1)}>Add</button>
</div>
)
}
function App() {
const [show, setShow] = useState(true)
return (
<div>
<button onClick={() => setShow(show => !show)}>Toggle</button>
{show && (
<KeepAlive>
<Counter />
</KeepAlive>
)}
</div>
)
}
export default App
注意:与 react-router
或 react-redux
配合使用时,建议将 <AliveScope>
放置在 <Router>
或 <Provider>
内部
// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { AliveScope } from 'react-activation'
import App from './App'
ReactDOM.render(
<AliveScope>
<App />
</AliveScope>,
document.getElementById('root')
)
ClassComponent
可配合 withActivation
装饰器
使用 componentDidActivate
与 componentWillUnactivate
对应激活与缓存两种状态
FunctionComponent
则分别使用 useActivate
与 useUnactivate
hooks 钩子
...
import KeepAlive, { useActivate, useUnactivate, withActivation } from 'react-activation'
@withActivation
class TestClass extends Component {
...
componentDidActivate() {
console.log('TestClass: componentDidActivate')
}
componentWillUnactivate() {
console.log('TestClass: componentWillUnactivate')
}
...
}
...
function TestFunction() {
useActivate(() => {
console.log('TestFunction: didActivate')
})
useUnactivate(() => {
console.log('TestFunction: willUnactivate')
})
...
}
...
function App() {
...
return (
{show && (
<KeepAlive>
<TestClass />
<TestFunction />
</KeepAlive>
)}
)
}
...
-
给需要控制缓存的
<KeepAlive />
标签增加name
属性 -
使用
withAliveScope
或useAliveController
获取控制函数-
drop(name): ("卸载"仅可用于缓存状态下的节点,如果节点没有被缓存但需要清空缓存状态,请使用 “刷新” 控制)
按 name 卸载缓存状态下的
<KeepAlive>
节点,name 可选类型为String
或RegExp
,注意,仅卸载命中<KeepAlive>
的第一层内容,不会卸载<KeepAlive>
中嵌套的、未命中的<KeepAlive>
-
dropScope(name): ("卸载"仅可用于缓存状态下的节点,如果节点没有被缓存但需要清空缓存状态,请使用 “刷新” 控制)
按 name 卸载缓存状态下的
<KeepAlive>
节点,name 可选类型为String
或RegExp
,将卸载命中<KeepAlive>
的所有内容,包括<KeepAlive>
中嵌套的所有<KeepAlive>
-
refresh(name):
按 name 刷新缓存状态下的
<KeepAlive>
节点,name 可选类型为String
或RegExp
,注意,仅刷新命中<KeepAlive>
的第一层内容,不会刷新<KeepAlive>
中嵌套的、未命中的<KeepAlive>
-
refreshScope(name):
按 name 刷新缓存状态下的
<KeepAlive>
节点,name 可选类型为String
或RegExp
,将刷新命中<KeepAlive>
的所有内容,包括<KeepAlive>
中嵌套的所有<KeepAlive>
-
clear():
将清空所有缓存中的 KeepAlive
-
getCachingNodes():
获取所有缓存中的节点
-
...
import KeepAlive, { withAliveScope, useAliveController } from 'react-activation'
...
<KeepAlive name="Test">
...
<KeepAlive>
...
<KeepAlive>
...
</KeepAlive>
...
</KeepAlive>
...
</KeepAlive>
...
function App() {
const { drop, dropScope, clear, getCachingNodes } = useAliveController()
useEffect(() => {
drop('Test')
// or
drop(/Test/)
// or
dropScope('Test')
clear()
})
return (
...
)
}
// or
@withAliveScope
class App extends Component {
render() {
const { drop, dropScope, clear, getCachingNodes } = this.props
return (
...
)
}
}
...
给需要控制缓存的 <KeepAlive />
标签增加 when
属性,取值如下
- true: 卸载时缓存
- false: 卸载时不缓存
<KeepAlive when={true}>
第 1 位参数表示是否需要在卸载时缓存
第 2 位参数表示是否卸载 <KeepAlive>
的所有缓存内容,包括 <KeepAlive>
中嵌套的所有 <KeepAlive>
// 例如:以下表示卸载时不缓存,并卸载掉嵌套的所有 `<KeepAlive>`
<KeepAlive when={[false, true]}>
...
<KeepAlive>
...
<KeepAlive>...</KeepAlive>
...
</KeepAlive>
...
</KeepAlive>
返回值为上述 Boolean
或 Array
,依照上述说明生效
但 when
的最终计算时机调整到 <KeepAlive>
组件 componentWillUnmount
时,可避免大部分 when 属性没有达到预期效果的问题
<KeepAlive when={() => true}>
<KeepAlive when={() => [false, true]}>
同一个父节点下,相同位置的 <KeepAlive>
默认会使用同一份缓存
例如下述的带参数路由场景,/item
路由会按 id
来做不同呈现,但只能保留同一份缓存
<Route
path="/item/:id"
render={props => (
<KeepAlive>
<Item {...props} />
</KeepAlive>
)}
/>
类似场景,可以使用 <KeepAlive>
的 id
属性,来实现按特定条件分成多份缓存
<Route
path="/item/:id"
render={props => (
<KeepAlive id={props.match.params.id}>
<Item {...props} />
</KeepAlive>
)}
/>
<KeepAlive />
会检测它的 children
属性中是否存在可滚动的元素,然后在 componentWillUnactivate
之前自动保存滚动位置,在 componentDidActivate
之后恢复保存的滚动位置
如果你不需要 <KeepAlive />
做这件事,可以将 saveScrollPosition
属性设置为 false
<KeepAlive saveScrollPosition={false} />
如果你的组件共享了屏幕滚动容器如 document.body
或 document.documentElement
, 将 saveScrollPosition
属性设置为 "screen"
可以在 componentWillUnactivate
之前自动保存共享屏幕容器的滚动位置
<KeepAlive saveScrollPosition="screen" />
将 <KeepAlive />
的 children
属性传递到 <AliveScope />
中,通过 <Keeper />
进行渲染
<Keeper />
完成渲染后通过 DOM
操作,将内容转移到 <KeepAlive />
中
由于 <Keeper />
不会被卸载,故能实现缓存功能
围绕最简实现,本仓库后续代码主要集中在
(源码欢迎微信讨论 375564567)
-
<KeepAlive />
中需要有一个将 children 传递到<AliveScope />
的动作,故真实内容的渲染会相较于正常情况慢一拍将会对严格依赖生命周期顺序的功能造成一定影响,例如
componentDidMount
中 ref 的取值,如下class Test extends Component { componentDidMount() { console.log(this.outside) // will log <div /> instance console.log(this.inside) // will log undefined } render() { return ( <div> <div ref={ref => { this.outside = ref }} > Outside KeepAlive </div> <KeepAlive> <div ref={ref => { this.inside = ref }} > Inside KeepAlive </div> </KeepAlive> </div> ) } }
ClassComponent
中上述错误可通过利用withActivation
高阶组件修复FunctionComponent
目前暂无处理方式,可使用setTimeout
或nextTick
延时获取ref
@withActivation class Test extends Component { componentDidMount() { console.log(this.outside) // will log <div /> instance console.log(this.inside) // will log <div /> instance } render() { return ( <div> <div ref={ref => { this.outside = ref }} > Outside KeepAlive </div> <KeepAlive> <div ref={ref => { this.inside = ref }} > Inside KeepAlive </div> </KeepAlive> </div> ) } }
-
对 Context 的破坏性影响
[email protected] 版本后 React 16.3+ 版本中已自动修复
[email protected] 版本配合 React 17+ 需要做如下调整以达到自动修复效果
import { autoFixContext } from 'react-activation' autoFixContext( [require('react/jsx-runtime'), 'jsx', 'jsxs', 'jsxDEV'], [require('react/jsx-dev-runtime'), 'jsx', 'jsxs', 'jsxDEV'] )
[email protected] 以下版本需手动修复,参考以下
问题情景参考:StructureBuilder/react-keep-alive#36
<Provider value={1}> {show && ( <KeepAlive> <Consumer> {( context // 由于渲染层级被破坏,此处无法正常获取 context ) => <Test contextValue={context} />} </Consumer> </KeepAlive> )} <button onClick={toggle}>toggle</button> </Provider>
修复方式任选一种
-
使用从
react-activation
导出的createContext
创建上下文 -
使用从
react-activation
导出的fixContext
修复受影响的上下文
... import { createContext } from 'react-activation' const { Provider, Consumer } = createContext() ... // or ... import { createContext } from 'react' import { fixContext } from 'react-activation' const Context = createContext() const { Provider, Consumer } = Context fixContext(Context) ...
-
-
对依赖于 React 层级的功能造成影响,如下
- react-router 的 withRouter/hooks 功能异常修正
-
Error Boundaries(已修复) -
React.Suspense & React.lazy(已修复) - React 合成事件冒泡失效
- 其他未发现的功能