React中实现keepalive组件缓存效果
Posted coder__wang
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React中实现keepalive组件缓存效果相关的知识,希望对你有一定的参考价值。
背景:由于react官方并没有提供缓存组件相关的api(类似vue中的keepalive),在某些场景,会使得页面交互性变的很差,比如在有搜索条件的表格页面,点击某一条数据跳转到详情页面,再返回表格页面,会重新请求数据,搜索条件也将清空,用户得重新输入搜索条件,再次请求数据,大大降低办公效率,如图:
目标:封装keepalive缓存组件,实现组件的缓存,并暴露相关方法,可以手动清除缓存。
版本:React 17,react-router-dom 5
结构:
代码:
cache-types.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-types";
function cacheReducer(cacheStates = , type, payload )
switch (type)
case cacheTypes.CREATE:
return
...cacheStates,
[payload.cacheId]:
scrolls: , // 缓存滚动条,key: dom, value: scrollTop
cacheId: payload.cacheId, // 缓存Id
element: payload.element, // 需要渲染的虚拟DOM
doms: undefined, // 当前的虚拟dom所对应的真实dom
status: cacheTypes.CREATE,// 缓存状态
,
;
case cacheTypes.CREATED:
return
...cacheStates,
[payload.cacheId]:
...cacheStates[payload.cacheId],
doms: payload.doms,
status: cacheTypes.CREATED,
,
;
case cacheTypes.ACTIVE:
return
...cacheStates,
[payload.cacheId]:
...cacheStates[payload.cacheId],
status: cacheTypes.ACTIVE,
,
;
case cacheTypes.DESTROY:
return
...cacheStates,
[payload.cacheId]:
...cacheStates[payload.cacheId],
status: cacheTypes.DESTROY,
,
;
default:
return cacheStates;
export default cacheReducer;
KeepAliveProvider.js
import React, useReducer, useCallback from "react";
import CacheContext from "./CacheContext";
import cacheReducer from "./cacheReducer";
import * as cacheTypes from "./cache-types";
function KeepAliveProvider(props)
let [cacheStates, dispatch] = useReducer(cacheReducer, );
const mount = useCallback(
( cacheId, element ) =>
// 挂载元素方法,提供子组件调用挂载元素
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, element ); // 创建缓存
else
dispatch( type: cacheTypes.CREATE, payload: cacheId, element ); // 创建缓存
,
[cacheStates]
);
let handleScroll = useCallback(
// 缓存滚动条
(cacheId, target ) =>
if (cacheStates[cacheId])
let scrolls = cacheStates[cacheId].scrolls;
scrolls[target] = target.scrollTop;
,
[cacheStates]
);
return (
<CacheContext.Provider
value= mount, cacheStates, dispatch, handleScroll
>
props.children
/* cacheStates维护所有缓存信息, dispatch派发修改缓存状态*/
Object.values(cacheStates)
.filter((cacheState) => cacheState.status !== cacheTypes.DESTROY)
.map(( cacheId, element ) => (
<div
id=`cache_$cacheId`
key=cacheId
// 原生div中声明ref,当div渲染到页面,会执行ref中的回调函数,这里在id为cache_$cacheId的div渲染完成后,会继续渲染子元素
ref=(dom) =>
let cacheState = cacheStates[cacheId];
if (
dom &&
(!cacheState.doms || cacheState.status === cacheTypes.DESTROY)
)
let doms = Array.from(dom.childNodes);
dispatch(
type: cacheTypes.CREATED,
payload: cacheId, doms ,
);
>
element
</div>
))
</CacheContext.Provider>
);
const useCacheContext = () =>
const context = React.useContext(CacheContext);
if (!context)
throw new Error("useCacheContext必须在Provider中使用");
return context;
;
export KeepAliveProvider, useCacheContext ;
withKeepAlive.js
import React, useContext, useRef, useEffect from "react";
import CacheContext from "./CacheContext";
import * as cacheTypes from "./cache-types";
function withKeepAlive(
OldComponent,
cacheId = window.location.pathname, scroll = false
)
return function (props)
const mount, cacheStates, dispatch, handleScroll =
useContext(CacheContext);
const ref = useRef(null);
useEffect(() =>
if (scroll)
// scroll = true, 监听缓存组件的滚动事件,调用handleScroll()缓存滚动条
ref.current.addEventListener(
"scroll",
handleScroll.bind(null, cacheId),
true
);
, [handleScroll]);
useEffect(() =>
let cacheState = cacheStates[cacheId];
if (
cacheState &&
cacheState.doms &&
cacheState.status !== cacheTypes.DESTROY
)
// 如果真实dom已经存在,且状态不是DESTROY,则用当前的真实dom
let doms = cacheState.doms;
doms.forEach((dom) => ref.current.appendChild(dom));
if (scroll)
// 如果scroll = true, 则将缓存中的scrollTop拿出来赋值给当前dom
doms.forEach((dom) =>
if (cacheState.scrolls[dom])
dom.scrollTop = cacheState.scrolls[dom];
);
else
// 如果还没产生真实dom,派发生成
mount(
cacheId,
element: <OldComponent ...props dispatch=dispatch />,
);
, [cacheStates, dispatch, mount, props]);
return <div id=`keepalive_$cacheId` ref=ref />;
;
export default withKeepAlive;
index.js
export KeepAliveProvider from "./KeepAliveProvider";
export default as withKeepAlive from './withKeepAlive';
使用:
1.用<KeepAliveProvider></KeepAliveProvider>将目标缓存组件或者父级包裹;
2.将需要缓存的组件,传入withKeepAlive方法中,该方法返回一个缓存组件;
3.使用该组件;
App.js
import React from "react";
import
BrowserRouter,
Link,
Route,
Switch,
from "react-router-dom";
import Home from "./Home.js";
import List from "./List.js";
import Detail from "./Detail.js";
import KeepAliveProvider, withKeepAlive from "./keepalive-cpn";
const KeepAliveList = withKeepAlive(List, cacheId: "list", scroll: true );
function App()
return (
<KeepAliveProvider>
<BrowserRouter>
<ul>
<li>
<Link to="/">首页</Link>
</li>
<li>
<Link to="/list">列表页</Link>
</li>
<li>
<Link to="/detail">详情页A</Link>
</li>
</ul>
<Switch>
<Route path="/" component=Home exact></Route>
<Route path="/list" component=KeepAliveList></Route>
<Route path="/detail" component=Detail></Route>
</Switch>
</BrowserRouter>
</KeepAliveProvider>
);
export default App;
效果:
假设有个需求,从首页到列表页,需要清空搜索条件,重新请求数据,即回到首页,需要清除列表页的缓存。
上面的KeepAliveProvider.js中,暴露了一个useCacheContext()的hook,该hook返回了缓存组件相关数据和方法,这里可以用于清除缓存:
Home.js
import React, useEffect from "react";
import DESTROY from "./keepalive-cpn/cache-types";
import useCacheContext from "./keepalive-cpn/KeepAliveProvider";
const Home = () =>
const cacheStates, dispatch = useCacheContext();
const clearCache = () =>
if (cacheStates && dispatch)
for (let key in cacheStates)
if (key === "list")
dispatch( type: DESTROY, payload: cacheId: key );
;
useEffect(() =>
clearCache();
// eslint-disable-next-line
, []);
return (
<div>
<div>首页</div>
</div>
);
;
export default Home;
效果:
至此,react简易版的keepalive组件已经完成啦~
脚踏实地行,海阔天空飞
如何在 React Redux 中实现自包含组件?
【中文标题】如何在 React Redux 中实现自包含组件?【英文标题】:How to implement a self contained component in react redux? 【发布时间】:2016-10-22 11:46:03 【问题描述】:我正在基于react redux构建一个文件管理器webui(我的目的是通过这个项目掌握react和redux)
如您所知,文件管理器需要一个树浏览器。我想构建一个可以包含它自己并且每个都有自己状态的组件。如下:
TreeNode
也可以包含TreeNode
的子代。每个TreeNode
都保持其状态path, children_nodes, right .....
,children_nodes
是从服务器获取的,path
是由父代传递的。这就是我的想象。
结构如下:
App:
TreeNode
--TreeNode
----TreeNode
----TreeNode
TreeNode
TreeNode
--TreeNode
TreeNode
--TreeNode
----TreeNode
----TreeNode
但是麻烦来了,因为reduxconnect
存储到树根,根下的所有节点接收到相同的状态...
例如,我有一个OPEN_NODE
动作,旨在触发getFileList fucntion
基于此节点的路径并将此节点的state.open
设置为true
。(注意:getFileList fucntion
尚未实现,只需给出暂时的假数据)
屏幕截图:
点击每个元素,但states are equal
。
我的代码:
容器/App.js
import React, Component, PropTypes from 'react';
import bindActionCreators from 'redux';
import connect from 'react-redux';
import Footer from '../components/Footer';
import TreeNode from '../containers/TreeNode';
import Home from '../containers/Home';
import * as NodeActions from '../actions/NodeActions'
export default class App extends Component
componentWillMount()
// this will update the nodes on state
this.props.actions.getNodes();
render()
const nodes = this.props
console.log(nodes)
return (
<div className="main-app-container">
<Home />
<div className="main-app-nav">Simple Redux Boilerplate</div>
<div>
nodes.map(node =>
<TreeNode key=node.name info=node actions=this.props.actions/>
)
</div>
/*<Footer />*/
</div>
);
function mapStateToProps(state)
return
nodes: state.opener.nodes,
open: state.opener.open
;
function mapDispatchToProps(dispatch)
return
actions: bindActionCreators(NodeActions, dispatch)
;
export default connect(
mapStateToProps,
mapDispatchToProps
)(App);
容器/TreeNode.js
import React, Component, PropTypes from 'react'
import bindActionCreators from 'redux'
import connect from 'react-redux'
import classNames from 'classnames/bind'
import * as NodeActions from '../actions/NodeActions'
export default class TreeNode extends Component
constructor(props, context)
super(props, context)
this.props =
open: false,
nodes: [],
info:
handleClick()
let open = this.props
if (open)
this.props.actions.closeNode()
else
this.props.actions.openNode()
render()
const actions, nodes, info = this.props
return (
<div className=classNames('tree-node', 'open':this.props.open) onClick= () => this.handleClick() >
<a>info.name</a>
nodes &&
<div>nodes.map(node => <TreeNode info=node />)</div>
!nodes &&
<div>no children</div>
</div>
);
TreeNode.propTypes =
open:PropTypes.bool,
info:PropTypes.object.isRequired,
nodes:PropTypes.array,
actions: PropTypes.object.isRequired
actions/NodeActions.js
import OPEN_NODE, CLOSE_NODE, GET_NODES from '../constants/NodeActionTypes';
export function openNode()
return
type: OPEN_NODE
;
export function closeNode()
return
type: CLOSE_NODE
;
export function getNodes()
return
type: GET_NODES
;
reducers/TreeNodeReducer.js
import OPEN_NODE, CLOSE_NODE, GET_NODES from '../constants/NodeActionTypes';
const initialState =
open: false,
nodes: [],
info:
const testNodes = [
name:'t1',type:'t1',
name:'t2',type:'t2',
name:'t3',type:'t3',
]
function getFileList()
return
nodes: testNodes
export default function opener(state = initialState, action)
switch (action.type)
case OPEN_NODE:
var nodes = getFileList()
return
...state,
open:true,
nodes:nodes
;
case CLOSE_NODE:
return
...state,
open:false
;
case GET_NODES:
var nodes = getFileList()
return
...state,
nodes:nodes
;
default:
return state;
完整代码见我的githubhttps://github.com/eromoe/simple-redux-boilerplate
我没有看到涵盖此类组件的示例,并且谷歌结果没有任何帮助。 有什么办法克服这个问题吗?
更新: 我看到了这个How to manage state in a tree component in reactjs
但解决方案是将整个树传递给状态,不能在文件管理器中使用。
【问题讨论】:
查看 Redux 中的computing derived data。 【参考方案1】:你所有的TreeNode
与redux 的状态相同,因为你的mapStateToProps
都是相同的。
mapStateToProps
可以将ownProps
(被包装组件的props
)作为第二个参数。你可以用它来区分你的TreeNodes
。在您的情况下,path
是一个不错的选择。
考虑编写像getChildrenNodes(state, path)
这样的状态选择器并相应地返回节点。
您可能需要考虑先阅读 redux 文档,尤其是本节:http://redux.js.org/docs/recipes/ComputingDerivedData.html
【讨论】:
【参考方案2】:我正在使用 React 和 Redux 实现一个类似 Github 的应用程序。
目前,我只列出存储库并显示其文件以及浏览它们。
我不知道这是一种好的做法还是坏的做法,但这就是我实现 Tree 组件的方式。
在每个树组件中,我都有一个指向自身的链接。而且我在路线上传递了一些数据,所以当我渲染它时我能够得到下一棵树。
组件
class Tree extends Component
constructor(props)
super(props);
this.renderList = this.renderList.bind(this);
componentWillMount()
this.props.getTree(this.props.params.sha);
componentWillReceiveProps(nextProps)
if(nextProps.params.sha !== this.props.params.sha)
this.props.getTree(nextProps.params.sha);
renderList(file)
return (
<tr key= file.sha >
file.type == 'tree'
? <td><Link to=`/repository/$this.props.params.repoName/tree/$file.path/$file.sha`> file.path </Link></td>
: <td><Link to=`/repository/$this.props.params.repoName/blob/$file.sha/$file.path`> file.path </Link></td>
</tr>
)
render()
const treeFile = this.props.tree;
const fileName = this.props.params.path;
return (
<div className="row">
<h3> fileName </h3>
<div className="col-md-12">
<table className="table table-hover table-bordered">
<tbody>
isEmpty(treeFile.tree) ? <tr>Loading</tr> : treeFile.tree.map(this.renderList)
</tbody>
</table>
</div>
</div>
)
export default Tree;
动作
const setTree = (tree) =>
return
type: actionTypes.GET_TREE,
tree
;
;
export const getTree = (sha) =>
return (dispatch, getState) =>
const repository, profile = getState();
const repo = GitHubApi.getRepo(profile.login, repository.name);
repo.getTree(sha, function(err, data)
dispatch(setTree(data));
);
减速器
const initialState = "";
export const tree = (state = initialState, action) =>
switch (action.type)
case actionTypes.GET_TREE:
return getTree(state, action);
return state;
const getTree = (state, action) =>
const tree = action;
return tree;
完整代码可以查看我的github仓库
https://github.com/glundgren93/Github-redux
【讨论】:
以上是关于React中实现keepalive组件缓存效果的主要内容,如果未能解决你的问题,请参考以下文章