Preface:

The company where the author works is a cross-border e-commerce company, and the main content of his work is ERPThis function.

We all know that Vue comes with keepAlive and its attribute include Exclude and vuex can easily realize the function of tags cache switch.

React does not officially support keepAlive, but there are a number of community solutions, based on what I wrote earlierCJY0208/react-router-cache-routeThis solution solves the caching problem.

Yarn add react-router configurepackage V6 as the default package.json version. React-router-cache-route does not currently support V6.

I have looked at a number of community solutions to solve the routing caching problem and have some understanding of the various caching solutions. Decided to implement a V6 support version of their own hence this article

No more talk, no more talkCode review

Effect:Realized functions: blacklist and whitelist/dynamic delete cache

Unfinished function

  • Life cycle (mainly not used in the author’s work app and therefore not motivated to add)

Code implementation

The core API React createPortal & React router useRoutes

/components/KeepAlive.tsx

import ReactDOM from 'react-dom'
import { equals, isNil, map, filter, not } from 'ramda'
import { useUpdate } from 'ahooks'
import {
 JSXElementConstructor,
 memo,
 ReactElement,
 RefObject,
 useEffect,
 useLayoutEffect,
 useRef,
 useState,
} from 'react'
type Children = ReactElement<any.string | JSXElementConstructor<any>> | null
interfaceProps { activeName? :string
 isAsyncInclude: boolean // Whether to add Include asynchronously if true is not specified will result in repeated renderinginclude? :Array<string> exclude? :Array<string> maxLen? :number
 children: Children
}
function KeepAlive({ activeName, children, exclude, include, isAsyncInclude, maxLen = 10 }: Props) {
 const containerRef = useRef<HTMLDivElement>(null)
 const components = useRef<ArrayThe < {name: string; ele: Children }>>([])
 const [asyncInclude] = useState<boolean>(isAsyncInclude)
 const update = useUpdate()
 useLayoutEffect(() = > {
  if (isNil(activeName)) {
   return
  }
  // If the cache exceeds the upper limit, the first cache is killed
  if (components.current.length >= maxLen) {
   components.current = components.current.slice(1)}/ / add
  const component = components.current.find((res) = > equals(res.name, activeName))
  if (isNil(component)) {
   components.current = [
    ...components.current,
    {
     name: activeName,
     ele: children,
    },
   ]
   if (not(asyncInclude)) {
    update()
   }
  }
  return () = > {  // Process the blacklist and whitelist
   if (isNil(exclude) && isNil(include)) {
    return
   }
   components.current = filter(({ name }) = > {
    if (exclude && exclude.includes(name)) {
     return false
    }
    if (include) {
     return include.includes(name)
    }
    return true
   }, components.current)
  }
 }, [children, activeName, exclude, maxLen, include, update, asyncInclude])
 return (
  <>
   <div ref={containerRef} className="keep-alive" />
   {map(
    ({ name, ele }) => (
     <Component active={equals(name, activeName)} renderDiv={containerRef} name={name} key={name}>
      {ele}
     </Component>
    ),
    components.current
   )}
  </>)}export default memo(KeepAlive)
interface ComponentProps {
 active: boolean
 children: Children
 name: string
 renderDiv: RefObject<HTMLDivElement>
}
// Render currently matched routes that do not match using createPortal to move inside document.createElement('div')
function Component({ active, children, name, renderDiv }: ComponentProps) {
 const [targetElement] = useState(() = > document.createElement('div'))
 const activatedRef = useRef(false)
 activatedRef.current = activatedRef.current || active
 useEffect(() = > {
  if (active) {// Render the matching componentrenderDiv.current? .appendChild(targetElement) }else {
   try { // Remove unrendered componentsrenderDiv.current? .removeChild(targetElement) }catch (e) {}
  }
 }, [active, name, renderDiv, targetElement])
 useEffect(() = > {// Adding an ID as an identifier doesn't do much
  targetElement.setAttribute('id', name)
 }, [name, targetElement])
 // Render vNode inside document.createElement('div')
 return <>{activatedRef.current && ReactDOM.createPortal(children, targetElement)}</>
}
export const KeepAliveComponent = memo(Component)
Copy the code

Vue components come with a name attribute and KeepAlive is based on the name cache. The React component does not need to manually pass in a name= activeName KeepAlive. The TSX component already implements cache functions as follows:

<KeepAlive activeName={currentKey}>
 {vnode}
</KeepAlive>
Copy the code

Using the React developer tool, we can see that KeepAlive caches all rendered VNodes. Then only the currently matched routing VNodes are rendered on the page

Routing rendering

The layout component gets its children inside the layout component

Layout.tsx

import { FunctionComponent, memo, Suspense, useCallback, useEffect, useMemo, useReducer } from 'react' import { BackTop, Layout as ALayout, Menu } from 'antd' import { Link, useLocation, useNavigate, useRoutes } from 'react-router-dom' import { equals, isNil, last, map } from 'ramda' import TagsView, { Action, ActionType, reducer } from './tagsView' import { Loading } from '@/components/Loading' import $styles from './tagsView/index.module.scss' import type { RouteMatch, RouteObject } from 'react-router' import KeepAlive from '@/components/KeepAlive' import { ViewProvider } from '@/hooks/useView' import { RouteConfig } from '@/router/configure' export interface RouteObjectDto extends RouteObject {  name: string meta? : { title: string } } function makeRouteObject(routes: RouteConfig[], dispatch: React.Dispatch<Action>): Array<RouteObjectDto> { return map((route) => { return { path: route.path, name: route.name, meta: route.meta, element: ( <ViewProvider value={{ name: route.name }}> <route.component name={route.name} dispatch={dispatch} /> </ViewProvider> ), children: isNil(route.children) ? undefined : makeRouteObject(route.children, dispatch), } }, routes) } function mergePtah(path: string, paterPath = '') { // let pat = getGoto(path) path = path.startsWith('/') ? path : Function renderMenu(data: Array<RouteConfig>, path?: string) { return map((route) => { const Icon = route.icon const thisPath = mergePtah(route.path, path) return route.alwaysShow ? null : isNil(route.children) ? ( <Menu.Item key={route.name} icon={Icon && <Icon />}> <Link to={thisPath}>{route.meta?.title}</Link> </Menu.Item> ) : ( <Menu.SubMenu title={route.meta?.title} key={route.name}> {renderMenu(route.children, thisPath)} </Menu.SubMenu> ) }, data) } interface Props { route: RouteConfig } function getLatchRouteByEle( ele: React.ReactElement<any, string | React.JSXElementConstructor<any>> ): RouteMatch<string>[] | null { const data = ele?.props.value return isNil(data.outlet) ? (data.matches as RouteMatch<string>[]) : getLatchRouteByEle(data.outlet) } const Layout: FunctionComponent<Props> = ({ route }: Props) => { const location = useLocation() const navigate = useNavigate() const [keepAliveList, dispatch] = useReducer(reducer, []) // Const routeObject = useMemo(() => {if (route.children) {return makeRouteObject(route.children, dispatch) } return [] }, [route.children]) // Match current path to render route const ele = useRoutes(routeObject) // Calculate matching route name const matchRouteObj = useMemo(() => { if (isNil(ele)) { return null } const matchRoute = getLatchRouteByEle(ele) if (isNil(matchRoute)) { return null } const selectedKeys: string[] = map((res) => { return (res.route as RouteObjectDto).name }, matchRoute) const data = last(matchRoute)?.route as RouteObjectDto return { key: last(matchRoute)?.pathname ?? '', title: data?.meta?.title ?? '', name: UseEffect (() => {if (matchRouteObj) {dispatch({type: ActionType.add, payload: { ... matchRouteObj, }, }) } else if (! equals(location.pathname, '/')) { navigate({ pathname: }}, [matchRouteObj, location, navigate]) const delKeepAlive = useCallback((key: string) => { dispatch({ type: ActionType.del, payload: { key, navigate, }, }) }, [navigate] ) const include = useMemo(() => { return map((res) => res.key, keepAliveList) }, [keepAliveList]) return ( <ALayout> <ALayout> <ALayout.Sider width={180} theme="light" className={$styles.fixed}> <Menu selectedKeys={matchRouteObj?.selectedKeys} defaultOpenKeys={matchRouteObj?.selectedKeys} mode="inline"> {renderMenu(route.children ?? [])} </Menu> </ALayout.Sider> <ALayout style={{ marginLeft: 180 }}> <TagsView delKeepAlive={delKeepAlive} keepAliveList={keepAliveList} /> <ALayout.Content className="app-content">  <Suspense fallback={<Loading />}> <KeepAlive activeName={matchRouteObj?.key} include={include} isAsyncInclude> {ele} </KeepAlive> </Suspense> </ALayout.Content> </ALayout> </ALayout> <BackTop /> </ALayout> ) } export default memo(Layout)Copy the code

The core code is that useRoutes can use useRoutes to get the ele of the route. We can use ele to get the information in the route. In this case, the pathname of the route is used as the unique name of the route

PS:

In digging gold to touch fish for more than two years, finally decided to write an article, the first time to write a text without what experience. Please excuse me mainly because I feel this kind of plan is ok. React-router-cache-route (router-cache-route