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组件缓存效果的主要内容,如果未能解决你的问题,请参考以下文章

在 React 中实现 keep alive

React中实现类似vue动态组件效果

状态改变时在 React JS 中实现过渡效果

在 React.js 中实现无状态子组件

如何在 AngularJS 中实现组件组合(类似于 React 渲染道具模式)?

vue 缓存组件keep-alive