preface
Vue officially provides keep-alive for caching components, while React does not, but third-party plugins do
This article sample code, online examples
NPM, address published
npm install keep-alive-comp
#or
yarn add keep-alive-comp
Copy the code
1. Function description
Generally speaking, keep-alive needs to do at least two things:
- Component State Recovery
- Component scroll position restored
2. Code implementation
Ideas:
- In routing/and elsewhere, functions act as
children
Parameters forAuxiliary functioncacheProps
That will beAuxiliary functionAttach to a component (e.g.Context.Consumer
Write it that way. - In the componentAppropriate location(such as jumping to another route) will scroll to the position
scrollTop
, need to savestate
Call as an argumentbeforeRouteLeave
- Go back to the current route/or the component is rendered again, the component is loaded after the callAuxiliary functionGet before
scrollTop
,state
Restoring to components
2.1 Auxiliary Functions
beforeRouteLeave
: called when the component is uninstalled, saving the scroll positionscrollTop
, state,state
scrollRestore
: called when returning to the component again to get the previously saved scroll positionscrollTop
stateRestore
: called when returning to the component again to get the previously saved statestate
deleteCache
: Clears scroll positions previously saved by the componentscrollTop
, state,state
By default, a maximum of five components can be cached- GetKeepAlive: Gets the parameters of the component cache
// Auxiliary functions
export interfaceKeepAliveAssist { beforeRouteLeave? :(scrollTop: number, state: any) = > void; scrollRestore? :(a)= > number | null; stateRestore? :(a)= > any; deleteCache? :(a)= > void; getKeepAlive? :(a)= > void;
}
Copy the code
2.2 Component Parameters
name
: Component tags, such as component nameschildren
: Component child elements, such as
<KeepAlive name="list">{(props) => <List {... props} />}</KeepAlive>
export interface KeepAliveProps {
name: string;
children: (cacheProps: KeepAliveAssist) = > React.ReactElement;
}
Copy the code
2.3 Body Code
KeepAlive
// src/index.ts
import { useEffect } from 'react';
import useKeepAliveCache from './useKeepAliveCache';
export interface KeepAliveProps {
name: string;
children: (cacheProps: KeepAliveAssist) = > React.ReactElement;
}
// Auxiliary functions
export interfaceKeepAliveAssist { beforeRouteLeave? :(scrollTop: number, state: any) = > void; scrollRestore? :(a)= > number | null; stateRestore? :(a)= > any; deleteCache? :(a)= > void; getKeepAlive? :(a)= > void;
}
export interface CacheItem {
name: string; state? :any; scrollTop? :number;
}
/** * component keep-alive * @param {*} name * @param {*} children */
const KeepAlive: React.FC<KeepAliveProps> = ({ name, children }) = > {
const isChildrenFunction = typeof children === 'function';
const { getItem, updateCache, deleteCache } = useKeepAliveCache();
useEffect((a)= > {
if(! isChildrenFunction) {console.warn(
'children pass function, such as :\n
{(props) => < list {... props} />}
'); }} []);const getKeepAlive = (a)= > {
return getItem(name);
};
// The component is called before the route changes
const beforeRouteLeave = (scrollTop: number = 0, state: any) = > {
updateCache({
name,
state,
scrollTop,
});
};
// Return to the scroll position
const scrollRestore = (a)= > {
const item = getItem(name);
returnitem? .scrollTop ||null;
};
// Returns the state of the component
const stateRestore = (a)= > {
const item = getItem(name);
returnitem? .state ||null;
};
const cacheProps: KeepAliveAssist = {
beforeRouteLeave,
scrollRestore,
stateRestore,
deleteCache: (a)= > deleteCache(name),
getKeepAlive,
};
return isChildrenFunction ? children(cacheProps) : null;
};
export default KeepAlive;
Copy the code
configKeepAlive
// src/configKeepAlive.ts
export interface ConfigProps {
store: any;
maxLength: number; useStorage? :'sessionStorage' | 'localStorage';
}
const CACHE_NAME = `__keep_alive_cache__`;
let DEFAULT_CONFIG: ConfigProps = {
store: window,
maxLength: 5,
useStorage: undefined};/ / configuration
const configKeepAlive = (props: Partial<ConfigProps> = {}) = > {
const init = (a)= >{ DEFAULT_CONFIG = { ... DEFAULT_CONFIG, ... props };const{ store, maxLength, useStorage } = DEFAULT_CONFIG; store[CACHE_NAME] = { maxLength, useStorage, cacheList: store[CACHE_NAME]? .cacheList || [], }; }; init();return{ cacheName: CACHE_NAME, ... DEFAULT_CONFIG, }; };export default configKeepAlive;
Copy the code
useKeepAliveCache
// src/useKeepAliveCache.ts
import { useEffect } from 'react';
import { CacheItem } from '. ';
import configKeepAlive from './configKeepAlive';
type UpdateStorageCache = {
_store: any;
_cacheName: string;
};
export type UpdateCache = {
name: string;
state: any;
scrollTop: number;
};
/ / cache
const useKeepAliveCache = (a)= > {
const { cacheName, maxLength, store, useStorage } = configKeepAlive();
const useStorageError = 'useStorage 'can only be: "sessionStorage","localStorage";
useEffect((a)= > {
if (useStorage) restoreCache();
elseclearOldStoraCache(); } []);const clearOldStoraCache = (a)= >{ store.sessionStorage? .removeItem(cacheName); store.localStorage? .removeItem(cacheName); };// Restore the cache from storage, if there is useStorage
const restoreCache = (a)= > {
const storageCache = getStorageCache();
if (storageCache) store[cacheName] = storageCache;
};
// Invalid useStorage
const inValidUseStorage = (): boolean= > {
return (
Boolean(useStorage) ! = =false&& useStorage ! = ='sessionStorage'&& useStorage ! = ='localStorage'
);
};
// Get storage cache
const getStorageCache = (
{ _store, _cacheName }: UpdateStorageCache = {
_store: store,
_cacheName: cacheName,
}
) => {
if (inValidUseStorage()) return console.warn(useStorageError);
let parsedCache: any = ' ';
constcache = _store[useStorage!] ? .getItem(_cacheName);if (cache) {
try {
parsedCache = JSON.parse(cache);
} catch (err) {
clearOldStoraCache();
console.error('Error recovering cache from storage, storage cache deleted! ', err); }}return parsedCache;
};
// Update the storage cache
const updateStorageCache = (
{ _store, _cacheName }: UpdateStorageCache = {
_store: store,
_cacheName: cacheName,
}
) => {
if (inValidUseStorage()) return console.warn(useStorageError); _store[useStorage!] ? .setItem( _cacheName,JSON.stringify({ ... _store[_cacheName], maxLength, useStorage }) ); };const getCacheList = (): CacheItem[] => {
const storeCache = store[cacheName];
return storeCache.cacheList;
};
const getItem = (name: string) = > {
let cacheList = getCacheList();
const item = cacheList.find((i: CacheItem) = > i.name === name);
return item || null;
};
// Add/update the cache
const updateCache = ({ name, scrollTop, state }: UpdateCache) = > {
let cacheList = getCacheList();
let index = cacheList.findIndex((i: CacheItem) = > i.name === name);
if(index ! = =- 1) {
cacheList.splice(index, 1, {
name,
state,
scrollTop,
});
} else {
cacheList.unshift({
name,
state,
scrollTop,
});
}
// Maximum cache length, 5 by default
if (cacheList.length > maxLength) cacheList.pop();
/ / update of storage
if (useStorage) updateStorageCache();
};
const deleteCache = (name: string) = > {
let cacheList = getCacheList();
let index = cacheList.findIndex((i: CacheItem) = > i.name === name);
if(index ! = =- 1) {
cacheList.splice(index, 1);
/ / update of storage
if(useStorage) updateStorageCache(); }};return {
getItem,
updateCache,
deleteCache,
getStorageCache,
};
};
export default useKeepAliveCache;
Copy the code
3, test,
Use the Jest + enzyme test
3.1 scripts – test
"scripts": {
"test": "cross-env NODE_ENV=test jest --config jest.config.js"
},
Copy the code
3.2 jest/enzyme
yarn add -D enzyme jest babel-jest enzyme enzyme-adapter-react-16
Copy the code
If you use typescript, download the types @types/enzyme, @types/jest
3.3 jest. Config. Js
//jest.config.js
module.exports = {
modulePaths: ['<rootDir>/src/'].moduleNameMapper: {
'.(css|less)$': '<rootDir>/__test__/NullModule.js',},collectCoverage: true.coverageDirectory: '<rootDir>/src/'.coveragePathIgnorePatterns: ['<rootDir>/__test__/'].coverageReporters: ['text']};Copy the code
3.4 __test__
index.test.js
// __test__/index.test.js import React from 'react'; import { configure, shallow } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import KeepAlive from '.. /src/index'; import configKeepAliveTest from './configKeepAliveTest'; import useKeepAliveCacheTest from './useKeepAliveCacheTest'; configure({ adapter: new Adapter() }); const Child = (props) => <div className="child">ccccaaaa</div>; describe('============= keep-alive test =============', () => { configKeepAliveTest({ store: global, maxLength: 2, useStorage: 'sessionStorage', }); useKeepAliveCacheTest('child'); const wrapper1 = shallow( <KeepAlive name="child">{(props) => <Child {... props} />}</KeepAlive> ); const wrapper2 = shallow( <KeepAlive name="child"> <Child /> </KeepAlive> ); It ('-- children '), () => {expect(typeof wrapper2.children() === 'function').tobe (false); expect(wrapper2.html()).toBe(null); }); / / it for the first time (' - successful rendering - '() = > renderSuccess (wrapper1)); It ('-- success attach attribute KeepAliveAssist to child component children --', () => addPropsSuccess(wrapper1)); It ('-- subcomponent, attached property KeepAliveAssist returns valid --', () => propsValid()); // Const renderSuccess = (_wrapper) => expect(_wrapper. Render ().text() === 'ccccaaaa').tobetruthy (); // Const renderSuccess = (_wrapper) => expect(_wrapper. Const addPropsSuccess = (_wrapper) => {const assistProps = ['beforeRouteLeave', 'scrollRestore', 'stateRestore', 'deleteCache', 'getKeepAlive', ]; const props = _wrapper.props(); const keys = Object.keys(props); const has = assistProps.every((key) => keys.includes(key)); expect(has).toBeTruthy(); }; let count = 0; KeepAliveAssist Returns valid const propsValid = () => {if (count > 1) return; count++; const { beforeRouteLeave, scrollRestore, stateRestore, deleteCache, getKeepAlive, } = wrapper1.props(); beforeRouteLeave(10, ['1', '2']); expect(scrollRestore()).toBe(10); expect(stateRestore()).toEqual(['1', '2']); const { name, scrollTop, state } = getKeepAlive(); expect(name).toBe('child'); expect(scrollTop).toBe(10); expect(state).toEqual(['1', '2']); // beforeRouteLeave(100, ['11', '22']); expect(scrollRestore()).toBe(100); expect(stateRestore()).toEqual(['11', '22']); const { name: name2, scrollTop: scrollTop2, state: state2, } = getKeepAlive(); expect(name2).toBe('child'); expect(scrollTop2).toBe(100); expect(state2).toEqual(['11', '22']); deleteCache(); expect(getKeepAlive()).toBe(null); }; });Copy the code
configKeepAliveTest.js
// __test__/configKeepAliveTest.js
import configKeepAlive from '.. /src/configKeepAlive';
function configKeepAliveTest(config) {
const { cacheName, store, maxLength, useStorage } = configKeepAlive(config);
it('-- configKeepAlive test --', () => {
expect(cacheName).toBe('__keep_alive_cache__');
expect(store).toBe(global);
expect(maxLength).toBe(config.maxLength);
expect(useStorage).toBe(config.useStorage);
});
}
export default configKeepAliveTest;
Copy the code
useKeepAliveCacheTest.js
// __test__/useKeepAliveCacheTest.js
import { renderHook } from '@testing-library/react-hooks';
import useKeepAliveCache from '.. /src/useKeepAliveCache';
import configKeepAlive from '.. /src/configKeepAlive';
function useKeepAliveCacheTest(name) {
it('-- useStorage: undefined test --', () = > {const useStorageValue = undefined;
const configKeepAliveProps = renderHook((a)= >
configKeepAlive({ store: global, useStorage: useStorageValue })
).result.current;
const useKeepAliveCacheProps = renderHook((a)= >useKeepAliveCache()).result .current; testHandler({ name, ... configKeepAliveProps, ... useKeepAliveCacheProps, useStorageValue, }); }); it('-- useStorage: sessionStorage 'test --', () = > {const useStorageValue = 'sessionStorage';
const configKeepAliveProps = renderHook((a)= >
configKeepAlive({ store: global, useStorage: useStorageValue })
).result.current;
const useKeepAliveCacheProps = renderHook((a)= >useKeepAliveCache()).result .current; testHandler({ name, ... configKeepAliveProps, ... useKeepAliveCacheProps, useStorageValue, }); }); }function testHandler({ name, cacheName, store, useStorage, getItem, updateCache, deleteCache, getStorageCache, useStorageValue, }) {
expect(useStorage).toBe(useStorageValue);
expect(getItem(name)).toBe(null);
const cache1 = { name, scrollTop: 10.state: { a: 'aa'}}; updateCache(cache1); expect(getItem(name)).toEqual(cache1);if (useStorageValue) {
const cache1 = getStorageCache({ _store: store, _cacheName: cacheName });
expect(Boolean(cache1)).not.toBeFalsy();
// Data in non-JSON format
store[useStorageValue].setItem(cacheName, 'dd');
const cache2 = getStorageCache({ _store: store, _cacheName: cacheName });
expect(cache2).toBe(' ');
const storeCache = store[useStorageValue].getItem(cacheName);
expect(storeCache).toBe(null);
// useStorage does not pass as required
} else {
const cache = getStorageCache();
expect(Boolean(cache)).toBeFalsy();
}
const cache2 = {
name,
scrollTop: 100.state: { a1: 'aa1'}}; updateCache(cache2); expect(getItem(name)).toEqual(cache2); deleteCache(name); expect(getItem(name)).toBe(null);
}
export default useKeepAliveCacheTest;
Copy the code
3.5 yarn test
Perform yarn test
zero9527@zero9527deMBP keep-alive (master) $ yarn testYarn Run v1.22.1 $cross-env NODE_ENV=test jest --config jest.config.js
PASS __test__/index.test.js
============= keep-alive test= = = = = = = = = = = = = ✓ configKeepAlive test - - (4 ms) ✓ useStorage: undefined test - 14 (ms) ✓ useStorage:"sessionStorage"✓ The test -- (31ms) ✓ -- children non-function not to render -- (2ms) ✓ -- successfully render -- (11ms) ✓ -- Successfully attach attribute KeepAliveAssist to child component children -- (4ms) ✓ -- Child component, Additional Properties KeepAliveAssist Return valid value -- (7ms) console.error Error recovering cache from storage, storage cache deleted! SyntaxError: Unexpected token din JSON at position 0
at JSON.parse (<anonymous>)
at getStorageCache (/Users/zero9527/Desktop/FE/keep-alive/src/useKeepAliveCache.ts:59:28)
at testHandler (/Users/zero9527/Desktop/FE/keep-alive/__test__/useKeepAliveCacheTest.js:64:20) at Object.<anonymous> (/Users/zero9527/Desktop/FE/keep-alive/__test__/useKeepAliveCacheTest.js:30:5) at Object.asyncJestTest (/Users/zero9527/Desktop/FE/keep-alive/node_modules/jest-jasmine2/build/jasmineAsyncInstall.js:100:37) at resolve (/Users/zero9527/Desktop/FE/keep-alive/node_modules/jest-jasmine2/build/queueRunner.js:45:12) at new Promise (<anonymous>) at mapper (/Users/zero9527/Desktop/FE/keep-alive/node_modules/jest-jasmine2/build/queueRunner.js:28:19) at promise.then (/Users/zero9527/Desktop/FE/keep-alive/node_modules/jest-jasmine2/build/queueRunner.js:75:41) 60 | } catch (err) { 61 | clearOldStoraCache(); > 62 | console.error('Error recovering cache from storage, storage cache deleted! ', err);
| ^
63 | }
64 | }
65 |
at getStorageCache (src/useKeepAliveCache.ts:62:17)
at testHandler (__test__/useKeepAliveCacheTest.js:64:20)
at Object.<anonymous> (__test__/useKeepAliveCacheTest.js:30:5)
----------------------|---------|----------|---------|---------|---------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - | -- -- -- -- -- -- -- -- - | -- -- -- -- -- -- -- -- -- - | -- -- -- -- -- -- -- -- - | -- -- -- -- -- -- -- -- - | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- All files | | | | 95.65 71.79 93.1 97.4 | configKeepAlive. Ts | 100 | 100 | 100 | 100 | index. The ts | | | | | 89.47 85.71 44.44 89.47 34 and 35 UseKeepAliveCache. Ts | 93.22 77.78 | | 100 | 100 | 34-40,53,76,113,121 ----------------------|---------|----------|---------|---------|--------------------- Test Suites: 1 passed, 1 total Tests: 7 passed, 7 total Snapshots: 0 Total Time: 2.399s, estimated 8s Ran alltestSuites. ✨ Donein4.08 s.Copy the code
4. Use examples
4.1 Routing File
// example/Router.tsx
import React, { Suspense } from 'react';
import { HashRouter, Route, Switch } from 'react-router-dom';
import { lazy } from '@loadable/component';
import KeepAlive from 'keep-alive-comp';
const List = lazy((a)= > import('./pages/list'));
const Detail = lazy((a)= > import('./pages/detail'));
const Router: React.FC = ({ children }) = >( <HashRouter> {children} <Switch> <Route exact path="/" component={() => ( <Suspense fallback={<div>loading... </div>}> <KeepAlive name="list">{(props) => <List {... props} />}</KeepAlive> </Suspense> )} /> <Route exact path="/detail/:id" component={() => ( <Suspense fallback={<div>loading... </div>}> <Detail /> </Suspense> )} /> <Route path="*" render={() => <h3>404</h3>} /> </Switch> </HashRouter> ); export default Router;Copy the code
4.2 list page
// example/pages/list.tsx
import React, { useEffect, useRef, useState } from 'react';
import { useHistory } from 'react-router';
import { KeepAliveAssist } from 'keep-alive';
import '.. /styles.css';
export interface ListProps extends KeepAliveAssist {}
const List: React.FC<ListProps> = ({
beforeRouteLeave,
scrollRestore,
stateRestore,
deleteCache,
}) => {
const history = useHistory();
const listRef = useRef<HTMLDivElement | null> (null);
const [scrollTop, setScrollTop] = useState(0);
const [list, updateList] = useState([]);
useEffect((a)= >{ restore(); } []);const restore = (a)= > {
const _scrollTop = scrollRestore();
const _state = stateRestore();
updateList(
(a)= >_state? .list || ['11111111111111111'.'22222222222222222'.'33333333333333333'.'44444444444444444'.'55555555555555555'.'66666666666666666',]); setTimeout((a)= > {
listRef.current.scrollTop = _scrollTop;
}, 0);
};
const onScroll = (e: any) = > {
e.persist();
const top = e.target.scrollTop;
setScrollTop(top);
const scrollHeight = listRef.current.scrollHeight;
const offsetHeight = listRef.current.offsetHeight;
if (scrollHeight - offsetHeight - top <= 50) {
const temp = new Array(5)
.fill(' ')
.map((i, index) = >
new Array(17).fill(`${list.length + index + 1}`).join(' ')); updateList((prev) = >[...prev, ...temp]); }};const toDetail = (i) = > {
beforeRouteLeave(scrollTop, { list });
history.push(`/detail/${i}`);
};
return (
<div className="list" ref={listRef} onScroll={onScroll}>
{list.map((i) => (
<div className="item" key={i} onClick={()= > toDetail(i)}>
{i}
</div>
))}
</div>
);
};
export default List;
Copy the code
The last
So that’s the end of it. Keep-alive is actually a very useful requirement, as I wrote earlier using display: None; , but need to transform the routing level, which is also complicated; While it does not automatically restore some states like Vue does, it is a way to do it without affecting other levels; It’s also a good plan