跳到主要内容

keepalive-react-component

语法


index.js

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
import List from "./list";
import Detail from "./detail";
import Home from "./home";
import { KeepAliveProvider, withKeepAlive } from "./keepalive-react-component";

const CacheHome = withKeepAlive(Home, { cacheId: "Home" });
const CacheList = withKeepAlive(List, { cacheId: "List", scroll: true });

function App() {
return (
<div>
<h3>React KeepAlive 实现</h3>
<BrowserRouter>
<KeepAliveProvider>
<ul>
<li>
<Link to="/">首页</Link>
</li>
<li>
<Link to="/list">列表</Link>
</li>
</ul>
<Routes>
<Route path="/" exact element={<CacheHome />}></Route>
<Route path="/list" exact element={<CacheList />}></Route>
<Route path="/detail/:id" exact element={<Detail />}></Route>
</Routes>
</KeepAliveProvider>
</BrowserRouter>
</div>
);
}

ReactDOM.render(<App />, document.getElementById("root"));

Home.js

import React from "react";

function Home(props) {
const { dispatch } = props;

const handleResetList = () => {
dispatch({
type: "DESTROY",
payload: {
cacheId: "List",
},
});
};

return (
<div>
<h3>首页</h3>
<button onClick={handleResetList}>重置 List 页面</button>
</div>
);
}

export default Home;

List.js

import React, { useState } from "react";
import { useNavigate } from "react-router-dom";

function List() {
const listData = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
const [list, setList] = useState(listData);
const navigate = useNavigate();
const handleClick = (id) => {
navigate("/detail/" + id);
};
return (
<div
style={{
height: "400px",
overflow: "auto",
}}
>
<h3>列表页</h3>
{list.map((item, index) => (
<li
key={index}
style={{ height: "100px", background: "red", marginBottom: "24px" }}
onClick={() => handleClick(item)}
>
{item}
</li>
))}
</div>
);
}

export default List;

Detail.js

import React from "react";
import { useParams } from "react-router-dom";

function Detail() {
const params = useParams();
const { id } = params;
return (
<div>
<h3>详情页</h3>
<p>{id}</p>
</div>
);
}

export default Detail;

实现


index.js

import withKeepAlive from "./withKeepAlive";
import KeepAliveProvider from "./keepAliveProvider";

export { KeepAliveProvider, withKeepAlive };

cache-type.js

export const CREATE = "CREATE";
export const CREATED = "CREATED";
export const ACTIVE = "ACTIVE";
export const DESTROY = "DESTROY"

cacheContext.js

import React from "react";

const CacheContext = React.createContext();

export default CacheContext;

cacheReducer.js

import * as cacheTypes from "./cache-type";

function cacheReducer(cacheStates, action) {
const { type, payload } = action;
const { cacheId, reactElement, doms } = payload;
switch (type) {
case cacheTypes.CREATE:
return {
...cacheStates,
[cacheId]: {
cacheId,
reactElement: reactElement,
doms: undefined,
status: cacheTypes.CREATE,
scrolls: {},
},
};
case cacheTypes.CREATED:
return {
...cacheStates,
[cacheId]: {
...cacheStates[cacheId],
doms: doms,
status: cacheTypes.CREATED,
},
};
case cacheTypes.DESTROY:
return {
...cacheStates,
[cacheId]: {
...cacheStates[cacheId],
status: cacheTypes.DESTROY,
},
};
default:
break;
}
}

export default cacheReducer;

keepAliveProvider.js

import React, { useCallback, useReducer } from "react";
import cacheReducer from "./cacheReducer";
import CacheContext from "./cacheContext";
import * as cacheTypes from "./cache-type";

function KeepAliveProvider(props) {
const [cacheStates, dispatch] = useReducer(cacheReducer, {});
const mount = useCallback(
({ cacheId, reactElement }) => {
if (cacheStates[cacheId]) {
let cacheState = cacheStates[cacheId];
if (cacheState.status === cacheTypes.DESTROY) {
let doms = cacheState.doms;
doms.forEach((dom) => dom.parentNode.removeChild(dom));
dispatch({
type: cacheTypes.CREATE,
payload: { cacheId, reactElement },
});
}
} else {
dispatch({
type: cacheTypes.CREATE,
payload: { cacheId, reactElement },
});
}
},
[cacheStates]
);
const handleScroll = useCallback(
(cacheId, event) => {
if (cacheStates[cacheId]) {
let target = event.target;
let scrolls = cacheStates[cacheId].scrolls;
scrolls[target] = target.scrollTop;
}
},
[cacheStates]
);
return (
<CacheContext.Provider
value={{ cacheStates, dispatch, mount, handleScroll }}
>
{props.children}
{Object.values(cacheStates)
.filter((cacheState) => cacheState.status !== cacheTypes.DESTROY)
.map(({ cacheId, reactElement }) => (
<div
key={cacheId}
ref={(divDOM) => {
let cacheState = cacheStates[cacheId];
if (
(divDOM && !cacheState.doms) ||
cacheState.status === cacheTypes.DESTROY
) {
let doms = Array.from(divDOM.childNodes);
dispatch({
type: cacheTypes.CREATED,
payload: { cacheId, doms },
});
}
}}
>
{reactElement}
</div>
))}
</CacheContext.Provider>
);
}

export default KeepAliveProvider;

withKeepAlive.js

import React, { useContext, useEffect, useRef } from "react";
import CacheContext from "./cacheContext";
import * as cacheTypes from "./cache-type";

function withKeepAlive(
WrappedComponent,
{ cacheId = window.location.pathname, scroll }
) {
return function (props) {
const divRef = useRef(null);
const { cacheStates, mount, dispatch, handleScroll } =
useContext(CacheContext);

useEffect(() => {
let onScroll = handleScroll.bind(null, cacheId);
if (scroll) {
divRef.current.addEventListener("scroll", onScroll, true);
return divRef.current.removeEventListener("scroll", onScroll);
}
}, [handleScroll]);

useEffect(() => {
const cacheState = cacheStates[cacheId];
if (
cacheState &&
cacheState.doms &&
cacheState.status !== cacheTypes.DESTROY
) {
const doms = cacheState.doms;
doms.forEach((dom) => divRef.current.appendChild(dom));
if (scroll) {
doms.forEach((dom) => {
if (cacheState.scrolls[dom]) {
dom.scrollTop = cacheState.scrolls[dom];
}
});
}
} else {
mount({
cacheId,
reactElement: <WrappedComponent {...props} dispatch={dispatch} />,
});
}
}, [cacheStates, mount, props]);

return <div ref={divRef}></div>;
};
}

export default withKeepAlive;