Some knowledge about the keep-alive function of react is here (on)
The next article is about " 大坑
" about this kind of plug-in, if you want to fully understand it, you must read the next article.
background
This is a requirement mentioned in the development in 2022 PM
, a certain table
is entered by the user with some search criteria and browsed to page 3, then if I jump to other After routing, return to the current page, I hope the search condition is still there, and it is still on the third page, isn't this the vue
keep-alive
tag in the ---c750f3deeb9708742c26b0516bc6e6a4---, but my current project uses the react
Written.
This time, I described my experience of "using external plug-ins" -> "abandoning external plug-ins" -> "learning and self-developed plug-ins" -> "understanding the dilemma of related plug-ins" -> "expecting react18
" Offscreen
😓", so the conclusion is that it is recommended to wait patiently for the self-support of react18
, but it is still very inspiring to learn the principles of current similar plugins.
A library can't just talk about its own advantages but also show its shortcomings, otherwise it will cause hidden dangers to the user's code, but I have read a lot of articles on the Internet and the official website, most of them do not tell the details of the relevant principles, and no one has the current The existing bug
will be analyzed. I will explain the related strange problems in detail here. The code I show below is slightly improved after referring to several solutions on the Internet.
1. Plug-in research
Let's take a look at what 'mature' solutions are currently on the market.
The first one: react-keep-alive : The official website is very formal, 851 Star, and its usage is also very close to vue's keep-alive, but there are too many bad reviews, and it has not been updated for 3 years, and many online articles are also Saying that this library is very pitiful, let's take a look at its comments (carry the next one).
The second one: react-router-cache-route : This library is a cache for the routing level and cannot take effect at the component level. After introduction, the current routing component library needs to be replaced. The risk is not small and the magnitude of the cache is too large (lifts the next one away).
The third: react-activation : This library is a library that everyone recognizes on the Internet, with fewer issues and not 'fatal', and can support component-level caching (in fact, it can't do it, there are still bugs), I tried After using it in the project of my own team, the effect is ok, but because this plug-in is not supported by a large team and the interior is all Chinese, it was not used in the end.
Through the above research, I became interested in the principle of react-activation
keep-alive
so I wanted to develop a similar plug-in within the team, wouldn't it? prelude.
Second, the core principle,
Let me repeat the premise first. The virtual DOM structure of react is a tree. The removal of a node of this tree will cause all child nodes to be destroyed, so it is necessary to wrap it with Memo when writing code. (remember this picture)
For example, I want to cache "B2组件"
state that in fact do is to let "B组件"
is destroyed "B2组件不被销毁"
, when seen from the chart "B组件"
it is destroyed "A组件"
will not be destroyed, because "A组件"
is not a subordinate of "B组件"
, so what we have to do is to let "A组件"
to generate "B2组件"
, and insert the --- "B2"
component into "B组件内部"
.
The so-called rendering under "A component" is inside "A组件"
:
function A(){
return (
<div>
<B1></B1>
</div>
)
}
Then use appendChild
div
transfer all the DOM elements in ---af10cdf729b70d5f9a95b5add8833a4d--- to "B组件"
.
3. React still executes normally after appendChild
Although appendChild
to "A组件"
inside dom
element into "B组件"
, but react
various rendering of the interior has been completed, for example, we "B1组件"
use within useState
defines a variable called 'n'
, when 'n'
when changes triggered dom
changes have also been recorded by react
, so it will not affect each element operation after dom diff
.
And "A组件"
The following may also be used "Consumer"
received "A组件"
outside "Provider"
, but also raises a problem that if not "A组件"
outside "Provider"
can not be received, the following is the processing method of react-actication
:
In fact, this kind of intrusion react
The operation of source code logic should be cautious, we can also use a vulgar way to replace it a little, mainly using Provider
The feature that can be written repeatedly, will Provider
and --- context
value
into it is normal, but it is obviously not friendly.
So react-activation
the official website will indicate the following paragraph:
Fourth, the plug-in architecture design introduction
First look at usage:
const RootComponent: React.FC = () => (
<KeepAliveProvider>
<Router>
<Routes>
<Route path={'/'} element={
<Keeper cacheId={'home'}> <Home/> </Keeper>
}
/>
</Routes>
</Router>
</KeepAliveProvider>
)
We use the KeepAliveProvider
component to store information about the component that needs to be cached, and to render the cached component, that is, to play the role of "A component".
KeepAliveProvider
internal component uses Keeper
assembly to mark the component should be rendered where? Is to use Keeper
the "B1组件"
+ "B2组件"
Wrap it up so we know where to put the initialized components.
cacheId
that is, cached id
, each id
corresponds to the cache information of a component, which will be used to monitor whether each cached component is "activated" , and cleaning the component cache.
Five, KeepAliveProvider development
Here is a "concept code" listed first, because looking directly at the complete code will make you faint.
import CacheContext from './cacheContext'
const KeepAliveProvider: React.FC = (props) => {
const [catheStates, dispatch]: any = useReducer(cacheReducer, {})
const mount = useCallback(
({ cacheId, reactElement }) => {
if (!catheStates || !catheStates[cacheId]) {
dispatch({
type: cacheTypes.CREATE,
payload: {
cacheId,
reactElement
}
})
}
},
[catheStates]
)
return (
<CacheContext.Provider value={{ catheStates, mount }}>
{props.children}
{Object.values(catheStates).map((item: any) => {
const { cacheId = '', reactElement } = item
const cacheState = catheStates[`${cacheId}`];
const handleDivDom = (divDom: Element) => {
const doms = Array.from(divDom.childNodes)
if (doms?.length) {
dispatch({
type: cacheTypes.CREATED,
payload: {
cacheId,
doms
}
})
}
}
return (
<div
key={`${cacheId}`}
id={`cache-外层渲染-${cacheId}`}
ref={(divDom) => divDom && handleDivDom(divDom)}>
{reactElement}
</div>
</CacheContext.Provider>
)
}
export default KeepAliveProvider
Code explanation
1. catheStates store all cached information
Its data format is as follows:
{
cacheId: 缓存id,
reactElement: 真正要渲染的内容,
status: 状态,
doms?: dom元素,
}
2. mount is used to initialize components
Change the state of the component to 'CREATE'
, and store the component to be rendered, which is in the picture above "B1组件"
,
const mount = useCallback(({ cacheId, reactElement }) => {
if (!catheStates || !catheStates[cacheId]) {
dispatch({
type: cacheTypes.CREATE,
payload: {
cacheId,
reactElement}
})
}
},
[catheStates]
)
3. CacheContext transfers and stores information
CacheContext
is specially created by us to store data, and he will distribute various methods to each Keeper.
import React from "react";
let CacheContext = React.createContext()
export default CacheContext;
4. {props.children} render the content in the KeepAliveProvider tag
5. div rendering components that need to be cached
Here is a div
as a container for rendering components. When we can get an instance of this div
, it will be stored in --- childNodes
to catheStates
, but there is a problem here, this writing method can only handle synchronously rendered subcomponents, if the component is rendered asynchronously, it cannot store the correct childNodes
.
6. Asynchronously rendered components
Assuming that there is an asynchronous component like the following, the correct dom
node cannot be obtained, so if the dom
of childNodes
is empty, we need to monitor dom
The state of dom
---, executed when an element is inserted in dom
.
function HomePage() {
const [show, setShow] = useState(false)
useEffect(() => {
setShow(true)
}, [])
return show ? <div>home</div>: null;
}
Make some modifications to the code of the handleDivDom method:
let initDom = false
const handleDivDom = (divDom: Element) => {
handleDOMCreated()
!initDom && divDom.addEventListener('DOMNodeInserted', handleDOMCreated)
function handleDOMCreated() {
if (!cacheState?.doms) {
const doms = Array.from(divDom.childNodes)
if (doms?.length) {
initDom = true
dispatch({
type: cacheTypes.CREATED,
payload: {
cacheId,
doms
}
})
}
}
}
}
When not acquired childNodes
div
Add "DOMNodeInserted"
events, to monitor whether there dom
inserted into div
internal.
So to sum up, the above code is responsible for initializing the relevant data and rendering the components, but we need to use the Keeper
component to render the specific components.
6. Write the Keeper that renders the placeholder
When using the plugin, the components we actually need to be cached are written in the Keeper
component, like the following way:
<Keeper cacheId="home">
<Home />
<User />
<footer>footer</footer>
</Keeper>
At this point we do not want to really Keeper
components to render the inside components, the props.children
stored in Keeper
which put a div
To occupy the space, and when it is detected that there is data that needs to be cached dom
, then use appendChild
to put dom
into its own interior.
import React, { useContext, useEffect } from 'react'
import CacheContext from './cacheContext'
export default function Keeper(props: any) {
const { cacheId } = props
const divRef = React.useRef(null)
const { catheStates, dispatch, mount } = useContext(CacheContext)
useEffect(() => {
const catheState = catheStates[cacheId]
if (catheState && catheState.doms) {
const doms = catheState.doms
doms.forEach((dom: any) => {
(divRef?.current as any)?.appendChild?.dom
})
} else {
mount({
cacheId,
reactElement: props.children
})
}
}, [catheStates])
return <div id={`keeper-原始位置-${cacheId}`} ref={divRef}></div>
}
There will be one more div
, I didn't find a good way, I tried to use doms
to replace this div
element, which will result in no react
data driver, and also try this dom
set "hidden = true"
then doms
inserted into this div
The sibling node of , but it was unsuccessful in the end.
Seven, Portals attribute introduction
I saw that some plug-ins on the Internet did not use appendChild
but used react
provided to achieve it, I feel very interesting, so I will talk about it here.
Portal
Provides an excellent solution for rendering child nodes to DOM
nodes that exist outside the parent component. To put it bluntly, I can specify that I want to child
Which dom
element to render to, the usage is as follows:
ReactDOM.createPortal(child, "目标dom")
The react official website describes it like this: A typical use case for a portal is when the parent component has overflow: hidden or z-index styles, but you need the child component to be able to visually "jump out" of its container. For example, dialogs, hover cards, and tooltips:
Since we need to specify where to render child
, so we need to have a clear child
attribute and target dom
, but our plugin may be more suitable for asynchronous operation, also That is, we just put the data in catheStates
, and fetch it when it needs to be fetched, instead of designing it in a clearly specified form when rendering.
Eight, monitoring cache is activated
We want to monitor which component is "activated" in real time. The definition of "activation" is that the component is cached after it is initialized, and each subsequent use of the cache is called "activation", and each time the component is activated, it is called activeCache
method to tell the user which component is currently "activated".
Why do you want to tell the user which component is activated? You can think of such a scenario, the user clicks the edit button of the third data of table
to jump to the editing page, and then returns to the list page after editing. We may need to update the status of the third item in the list, at which point we need to know which components are activated.
There is another situation as shown in the figure below. This is a kind of prompt that will appear when you hover the mouse tip
. If you click the button at this time to jump to the page, it will cause this. When you return to the list page, this tip
still....
Of course I'm not referring to element-ui
, it's our own ui
library, I looked at the reason at the time, because this component will only let the tip
disappeared, but the page jumped and all the current page dom
keep-alive
was cached by---e01eacdcd3c508c5846fc31422291148---was not cached, which led to this tip
.
Its code is as follows:
` useEffect(() => {
const catheState = catheStates[cacheId]
if (catheState && catheState.doms) {
console.log('激活了:', cacheId)
activeCache(cacheId)
}
}, [])
The reason why the parameter of useEffect
only passes an empty array, because every time the component is "activated", it can be executed, because every time Keeper
The component will be destroyed every time, so here can be executed.
Final use demo
It is used in the component to detect whether the specified component is updated. The first parameter is to monitor id
, which is Keeper
on cacheId
, and the second The parameters are callback
.
When users use the plug-in, they can monitor in their own components as follows:
useEffect(() => {
const cb = () => {
console.log('home被激活了')
}
cacheWatch(['home'], cb)
return () => {
removeCacheWatch(['home'], cb)
}
}, [])
Implementation
Define the activeCache method in KeepAliveProvider:
Every time the component is activated, it goes to the array to find the listener method to execute.
const [activeCacheObj, setActiveCacheObj] = useState<any>({})
const activeCache = useCallback(
(cacheId) => {
if (activeCacheObj[cacheId]) {
activeCacheObj[cacheId].forEach((fn: any) => {
fn(cacheId)
})
}
},
[catheStates, activeCacheObj]
)
Add a detection method:
Each time the callback is placed on the corresponding object.
const cacheWatch = useCallback(
(ids: string[], fn) => {
ids.forEach((id: string) => {
if (activeCacheObj[id]) {
activeCacheObj[id].push(fn)
} else {
activeCacheObj[id] = [fn]
}
})
setActiveCacheObj({
...activeCacheObj
})
},
[activeCacheObj]
)
There is also a method to remove monitoring:
const removeCacheWatch = (ids: string[], fn: any) => {
ids.forEach((id: string) => {
if (activeCacheObj[id]) {
const index = activeCacheObj[id].indexOf(fn)
activeCacheObj.splice(index, 1)
}
})
setActiveCacheObj({
...activeCacheObj
})
}
To delete the cache, you need to add a delete method in the cacheReducer. Note that you need to remove all DOMs here, instead of only deleting the data in cacheStates.
case cacheTypes.DESTROY:
if (cacheStates[payload.cacheId]) {
const doms = cacheStates?.[payload.cacheId]?.doms
if (doms) {
doms.forEach((element) => {
element.remove()
})
}
}
delete cacheStates[payload.cacheId]
return {
...cacheStates
}
end
The next article is about " 大坑
" about this kind of plug-in, if you want to fully understand it, you must read the next article, this is the case, I hope to make progress with you.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。