23
头图

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).

image.png

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)

image.png

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 :

image.png

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:

image.png

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 .

image.png

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.


lulu_up
5.7k 声望6.9k 粉丝

自信自律, 终身学习, 创业者