问题
最近在实现发帖论坛时,想实现这么一个效果
从列表页A跳转A的详情页,列表页A缓存
但在 React
中,我们通常会使用路由去管理不同的页面,而在切换页面时,路由将会卸载掉未匹配的页面组件,所以当用户从详情页退回列表页时,会回到列表页顶部,因为列表页组件被路由卸载后重建了,状态被丢失。React
中并没有这个功能,曾经有人在官方提过功能 issues ,但官方认为这个功能容易造成内存泄露,表示暂时不考虑支持,所以我们需要自己想办法了。那么如何实现如何实现 React 中的状态保存呢?
解决
查阅资料后,发现使用react-activation可以符合此场景,并且探究了一下他的原理。
如何使用
1.用AliveScope包裹App组件(就类似于redux中的provider)
2.用KeepAlive包裹要缓存的组件。
import React, { useState } from 'react'
import { render } from 'react-dom'
import KeepAlive, { AliveScope } from './KeepAlive'
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
count: {count}
<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 id="Test">
<Counter />
</KeepAlive>
)}
</div>
)
}
render(
<AliveScope>
<App />
</AliveScope>,
document.getElementById('root')
)
原理
由于React
会卸载掉处于固有组件层级内的组件,所以我们将<KeepAlive>
中的组件,也就是其children
属性抽取出来,渲染到一个不会被卸载的组件<AliveScope>
内,也就是保存在<AliveScope>
的state
状态里,再使用DOM
操作dom.appendChild
将<AliveScope>
内的真实内容移入对应<KeepAlive>
,就可以实现此功能
代码模拟实现
在网上只找到了class版本,自己对照着原理用hooks实现了一遍,目前没发现什么bug
import React, { createContext, useEffect, useRef, useState } from 'react'
const { Provider, Consumer } = createContext()
// 为了把AliveScope里的keep属性加到KeepAlive组件上去
const withScope = (WrappedComponent) => (props) => (
<Consumer>{(keep) => <WrappedComponent {...props} keep={keep} />}</Consumer>
)
export const AliveScope = (props) => {
const nodes = useRef({});
const [state, setState] = useState({});
const resolvePromise = useRef();
const firstUpdate = useRef(true);
const keep = (id, children) => {
const p = new Promise((resolve) => {
setState({
...state,
[id]: { id, children }
});
resolvePromise.current = () => resolve(nodes.current[id]);
})
return p;
}
// 这里要注意忽略首次更新所产生的useEffect,因为首次组件首次挂在时state为空,所以nodes这个ref也为空,取不到nodes.current[id]。
useEffect(() => {
if (firstUpdate.current) {
firstUpdate.current = false;
return;
}
resolvePromise.current();
}, [state])
return (
<Provider value={keep}>
{props.children}
{
Object.values(state).map(({ id, children }) => {
return (
<div
key={id}
id={id}
ref={(node) => {
// 这里用ref的函数形式,因为被keepAlive包裹的的组件不止一个
nodes.current[id] = node
}}
>
{children}
</div>
)
})
}
</Provider>
)
}
const KeepAlive = ({ id, children, keep }) => {
const placeholder = useRef();
useEffect(() => {
(async () => {
const realContent = await keep(id, children)
placeholder.current.appendChild(realContent)
})()
}, [])
return (
<div
ref={placeholder}
className="keepAlive"
/>
)
}
export default withScope(KeepAlive)