Commit a089fc4a authored by Shen Chang's avatar Shen Chang

Initial release.

parents
{
"presets": [
"@babel/env",
"@babel/react"
],
"plugins": [
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
],
"@babel/plugin-proposal-class-properties"
]
}
# editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
node_modules
coverage
es/**
.DS_Store
.travis.yml
.babelrc
.editorconfig
test
demo
node_modules
coverage
src
tsconfig.json
webpack.config.js
tslint.json
jest.config.js
language : node_js
node_js:
- "11"
install:
- npm install
script:
- npm test
after_success:
- npm run codecov
All notable changes are described on the [Releases](https://github.com/Sam618/react-keep-alive/releases) page.
MIT License
Copyright (c) Shen Chang 2019-2020
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
\ No newline at end of file
# React Keep Alive
Writing.
<!doctype html>
<html>
<head>
<title>react-keep-alive</title>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport" />
</head>
<body>
<div id="root"></div>
</body>
</html>
import React from 'react';
import ReactDOM from 'react-dom';
import {
Switch,
Route,
Link,
BrowserRouter as Router,
} from 'react-router-dom';
import {Provider} from '../../es';
import A from './views/A';
import B from './views/B';
import C from './views/C';
class App extends React.Component {
state = {
toggle: true,
};
handleClick = () => {
this.setState(({toggle}) => ({
toggle: !toggle,
}));
}
handleClickB = () => {
this.setState({
toggle: true,
});
}
handleClickC = () => {
this.setState({
toggle: false,
});
}
render() {
const {toggle} = this.state;
return (
<div>
<ul>
<li>
<Link to="/a">a</Link>
</li>
<li onClick={this.handleClickB}>
<Link to="/b">b</Link>
</li>
<li onClick={this.handleClickC}>
<Link to="/c">c</Link>
</li>
</ul>
<div>
<button onClick={this.handleClick}>toggle({toggle.toString()})</button>
</div>
<Switch>
<Route
path="/a"
render={() => (
<React.Fragment>
<A keepAlive={toggle} />
</React.Fragment>
)}
/>
<Route
path="/b"
render={() => <B />}
/>
<Route
path="/c"
render={() => <C />}
/>
</Switch>
</div>
);
}
}
ReactDOM.render(
(
<Provider>
<Router>
<App />
</Router>
</Provider>
),
document.getElementById('root')
);
import React from 'react';
import {keepAlive, bindLifecycle} from '../../../es';
@bindLifecycle
class Content extends React.Component {
componentWillMount() {
console.log('A Content componentWillMount');
}
componentDidMount() {
console.log('A Content componentDidMount');
}
componentDidActivate() {
console.log('A Content componentDidActivate');
}
componentWillUpdate() {
console.log('A Content componentWillUpdate');
}
componentDidUpdate() {
console.log('A Content componentDidUpdate');
}
componentWillUnactivate() {
console.log('A Content componentWillUnactivate');
}
componentWillUnmount() {
console.log('A Content componentWillUnmount');
}
render() {
console.log('A Content render');
console.log(this);
return (
<div>This is a content.</div>
);
}
}
@keepAlive()
class Test extends React.Component {
state = {
index: 0,
};
componentWillMount() {
console.log('A componentWillMount');
}
componentDidMount() {
console.log('A componentDidMount');
}
componentDidActivate() {
console.log('A componentDidActivate');
}
componentWillUpdate() {
console.log('A componentWillUpdate');
}
componentDidUpdate() {
console.log('A componentDidUpdate');
}
componentWillUnactivate() {
console.log('A componentWillUnactivate');
}
componentWillUnmount() {
console.log('A componentWillUnmount');
}
handleClick = () => {
this.setState(({index}) => ({index: ++index}));
};
render() {
console.log('A render');
return (
<div>
<div>This is a.</div>
<button onClick={this.handleClick}>click me({this.state.index})</button>
<Content />
</div>
);
}
}
export default Test;
import React from 'react';
export default class Test extends React.Component {
componentWillMount() {
console.log('B componentWillMount');
}
componentDidMount() {
console.log('B componentDidMount');
}
componentDidActivate() {
console.log('B componentDidActivate');
}
componentWillUpdate() {
console.log('B componentWillUpdate');
}
componentDidUpdate() {
console.log('B componentDidUpdate');
}
componentWillUnactivate() {
console.log('B componentWillUnactivate');
}
componentWillUnmount() {
console.log('B componentWillUnmount');
}
render() {
console.log('B render');
return (
<div>This is b.</div>
);
}
}
\ No newline at end of file
import React from 'react';
export default class Test extends React.Component {
componentWillMount() {
console.log('C componentWillMount');
}
componentDidMount() {
console.log('C componentDidMount');
}
componentDidActivate() {
console.log('C componentDidActivate');
}
componentWillUpdate() {
console.log('C componentWillUpdate');
}
componentDidUpdate() {
console.log('C componentDidUpdate');
}
componentWillUnactivate() {
console.log('C componentWillUnactivate');
}
componentWillUnmount() {
console.log('C componentWillUnmount');
}
render() {
console.log('C render');
return (
<div>This is c.</div>
);
}
}
\ No newline at end of file
module.exports = {
preset: 'ts-jest',
verbose: true,
testPathIgnorePatterns: [
'/node/modules',
],
setupFiles: [
'<rootDir>/test/setup.js',
],
transform: {
"^.+\\.js$": "babel-jest",
".(ts|tsx)": "ts-jest",
},
collectCoverage: true,
coverageDirectory: './coverage',
};
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "react-keep-alive",
"version": "0.1.0",
"description": "Keep the state of the component.",
"author": "Shen Chang",
"homepage": "https://github.com/Sam618/react-keep-alive",
"keywords": [
"react",
"keep-alive"
],
"repository": {
"type": "git",
"url": "git+https://github.com/Sam618/react-keep-alive.git"
},
"bugs": {
"url": "https://github.com/Sam618/react-keep-alive/issues"
},
"main": "es/index.js",
"scripts": {
"clean": "rimraf es",
"test": "jest",
"codecov": "codecov",
"build:demo": "webpack",
"build:es": "npm run clean && tsc",
"start:demo": "webpack-dev-server --hot --historyApiFallback",
"start:es": "npm run clean && tsc -w -sourcemap --outDir es"
},
"husky": {
"hooks": {
"pre-commit": "npm run test"
}
},
"license": "MIT",
"sideEffects": false,
"dependencies": {
"hoist-non-react-statics": "^3.3.0",
"js-md5": "^0.7.3"
},
"devDependencies": {
"@babel/cli": "^7.2.3",
"@babel/core": "^7.3.4",
"@babel/plugin-proposal-class-properties": "^7.3.0",
"@babel/plugin-proposal-decorators": "^7.3.0",
"@babel/preset-env": "^7.3.4",
"@babel/preset-react": "^7.0.0",
"@types/node": "^10.12.21",
"@types/react": "^16.8.1",
"babel": "^6.23.0",
"babel-loader": "^8.0.5",
"codecov": "^3.2.0",
"cz-conventional-changelog": "^2.1.0",
"enzyme": "^3.8.0",
"enzyme-adapter-react-16": "^1.9.1",
"html-webpack-plugin": "^3.2.0",
"husky": "^1.3.1",
"jest": "^24.1.0",
"react": "^16.3.0",
"react-dom": "^16.3.0",
"react-router-dom": "^4.3.1",
"rimraf": "^2.6.3",
"ts-jest": "^23.10.5",
"typescript": "^3.3.1",
"webpack": "^4.29.3",
"webpack-cli": "^3.2.3",
"webpack-dev-server": "^3.1.14"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
}
import React from 'react';
import ReactDOM from 'react-dom';
import noop from '../utils/noop';
export default class Comment extends React.PureComponent {
parentNode = null;
currentNode = null;
commentNode = null;
content = null;
componentDidMount() {
const node = ReactDOM.findDOMNode(this);
const commentNode = this.createComment();
this.commentNode = commentNode;
this.currentNode = node;
this.parentNode = node.parentNode;
this.parentNode.replaceChild(commentNode, node);
ReactDOM.unmountComponentAtNode(node);
this.props.onLoaded();
}
componentWillUnmount() {
this.parentNode.replaceChild(this.currentNode, this.commentNode);
}
createComment() {
let content = this.props.children;
if (typeof content !== 'string') {
content = '';
}
content = content.trim();
this.content = content;
return document.createComment(content);
}
render() {
return <div />;
}
}
Comment.defaultProps = {
onLoaded: noop,
};
\ No newline at end of file
import React from 'react';
import Comment from './Comment';
import findDOMNodeByFiberNode from '../utils/findDOMNodeByFiberNode';
import createUniqueIdentification from '../utils/createUniqueIdentification';
export const LIFECYCLE = {
MOUNTED: 0,
UPDATING: 1,
UNMOUNTED: 2,
};
class Consumer extends React.PureComponent {
renderElement = null;
identification = this.props.identification;
// This attribute is designed to prevent duplicates of the identification of KeepAlive components.
key = createUniqueIdentification();
constructor(props) {
super(props);
const {cache, setCache, children} = props;
if (!cache || !setCache) {
throw new Error('<KeepAlive> component must be in the <Provider> component.');
}
React.Children.only(children);
}
componentDidMount() {
const {
setCache,
children,
keepAlive,
} = this.props;
const {_reactInternalFiber} = this;
this.renderElement = findDOMNodeByFiberNode(_reactInternalFiber);
setCache(this.identification, {
children,
keepAlive,
lifecycle: LIFECYCLE.MOUNTED,
key: this.key,
renderElement: this.renderElement,
activated: true,
});
}
componentDidUpdate() {
const {
setCache,
children,
keepAlive,
} = this.props;
setCache(this.identification, {
children,
keepAlive,
lifecycle: LIFECYCLE.UPDATING,
});
}
componentWillUnmount() {
const {unactivate} = this.props;
unactivate(this.identification);
}
render() {
const {identification} = this;
return <Comment>{identification}</Comment>;
}
}
export default Consumer;
\ No newline at end of file
import React from 'react';
import ReactDOM from 'react-dom';
import Comment from './Comment';
import {LIFECYCLE} from './Consumer';
import KeepAliveContext from '../contexts/KeepAliveContext';
import createEventEmitter from '../utils/createEventEmitter';
import createUniqueIdentification from '../utils/createUniqueIdentification';
import createStoreElement from '../utils/createStoreElement';
export const keepAliveProviderTypeName = 'KeepAliveProvider';
export const START_MOUNTING_DOM = 'startMountingDOM';
// TODO: include max exclude
export default class KeepAliveProvider extends React.PureComponent {
storeElement = createStoreElement();
// Sometimes data that changes with setState cannot be synchronized, so force refresh
cache = Object.create(null);
keys = [];
eventEmitter = createEventEmitter();
existed = true;
needRerender = false;
providerIdentification = createUniqueIdentification();
componentDidUpdate() {
if (this.needRerender) {
this.needRerender = false;
this.forceUpdate();
}
}
componentWillUnmount() {
this.eventEmitter.clear();
this.existed = false;
document.body.removeChild(this.storeElement);
}
isExisted = () => {
return this.existed;
};
setCache = (identification, value) => {
const {cache, keys} = this;
const {key} = cache[identification] || {};
if (!key) {
keys.push(identification);
// this.shiftKey();
}
if (key && value.key && key !== value.key) {
throw new Error('Cached components have duplicates.');
}
this.cache[identification] = {
...cache[identification],
...value,
};
this.forceUpdate();
};
getMax = () => {
return this.props.max ? parseInt(this.props.max) : null;
};
shiftKey = () => {
const max = this.getMax();
const {keys, cache} = this;
if (!max || keys.length <= max) {
return;
}
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const currentCache = cache[key];
if (currentCache && !currentCache.activated) {
keys.splice(i, 1);
delete cache[key];
return;
}
}
}
unactivate = identification => {
const {cache} = this;
this.cache[identification] = {
...cache[identification],
key: null,
activated: false,
lifecycle: LIFECYCLE.UNMOUNTED,
};
this.forceUpdate();
};
startMountingDOM = identification => {
this.eventEmitter.emit([identification, START_MOUNTING_DOM]);
};
render() {
const {
cache,
providerIdentification,
isExisted,
setCache,
unactivate,
shiftKey,
storeElement,
eventEmitter,
} = this;
const {
children,
include,
exclude,
} = this.props;
return (
<KeepAliveContext.Provider
value={{
cache,
providerIdentification,
isExisted,
setCache,
shiftKey,
unactivate,
storeElement,
eventEmitter,
include,
exclude,
}}
>
<React.Fragment>
{children}
{
Object.entries(cache).map((
[
identification,
{
keepAlive,
children,
lifecycle,
},
],
) => {
const currentCache = cache[identification];
let cacheChildren = children;
if (lifecycle === LIFECYCLE.MOUNTED && !keepAlive) {
// If the cache was last enabled, then the components of this keepAlive package are used,
// and the cache is not enabled, the UI needs to be reset.
cacheChildren = null;
this.needRerender = true;
currentCache.lifecycle = LIFECYCLE.UPDATING;
}
// current true, previous true | undefined, keepAlive false, not cache
// current true, previous true | undefined, keepAlive true, cache
// current true, previous false, keepAlive true, cache
// current true, previous false, keepAlive false, not cache
return ReactDOM.createPortal(
(
cacheChildren
? (
<React.Fragment>
<Comment>{identification}</Comment>
{cacheChildren}
<Comment
onLoaded={() => this.startMountingDOM(identification)}
>{identification}</Comment>
</React.Fragment>
)
: null
),
storeElement,
);
})
}
</React.Fragment>
</KeepAliveContext.Provider>
);
}
}
\ No newline at end of file
import React from 'react';
const WithKeepAliveContext = React.createContext();
export default WithKeepAliveContext;
\ No newline at end of file
import React from 'react';
const KeepAliveContext = React.createContext();
export default KeepAliveContext;
\ No newline at end of file
import Provider from './components/Provider';
import keepAlive from './utils/keepAlive';
import bindLifecycle from './utils/bindLifecycle';
export {
Provider,
keepAlive,
bindLifecycle,
};
\ No newline at end of file
import React from 'react';
import hoistNonReactStatics from 'hoist-non-react-statics';
import IdentificationContext from '../contexts/IdentificationContext';
import Consumer, {LIFECYCLE} from '../components/Consumer';
import {START_MOUNTING_DOM} from '../components/Provider';
import md5 from './md5';
import noop from './noop';
import getContextIdentificationByFiberNode from './getContextIdentificationByFiberNode';
import withIdentificationConsumer from './withIdentificationContextConsumer';
import withKeepAliveConsumer from './withKeepAliveContextConsumer';
import changePositionByComment from './changePositionByComment';
import getDisplayName from './getDisplayName';
import getKeepAlive from './getKeepAlive';
export const COMMAND = {
UNACTIVATE: 'unactivate',
UNMOUNT: 'unmount',
ACTIVATE: 'activate',
};
export default function keepAlive({
name,
forwardRef = false,
} = {}) {
return Component => {
const {
componentDidMount = noop,
componentDidUpdate = noop,
componentDidActivate = noop,
componentWillUnactivate = noop,
componentWillUnmount = noop,
} = Component.prototype;
const displayName = name || getDisplayName(Component);
if (!displayName) {
throw new Error('Each component must have a name, which can be the component\'s displayName or name static property. You can also configure name when keepAlive decorates the component.');
}
Component.prototype.componentDidMount = function () {
const {
_container,
keepAlive,
} = this.props;
const {
notNeedActivate,
identification,
eventEmitter,
} = _container;
notNeedActivate();
const cb = () => {
mount.call(this);
eventEmitter.off([identification, START_MOUNTING_DOM], cb);
};
eventEmitter.on([identification, START_MOUNTING_DOM], cb);
componentDidMount.call(this);
if (keepAlive) {
componentDidActivate.call(this);
}
};
Component.prototype.componentDidUpdate = function () {
componentDidUpdate.call(this);
const {
_container,
} = this.props;
const {
notNeedActivate,
isNeedActivate,
} = _container;
if (isNeedActivate()) {
notNeedActivate();
mount.call(this);
this._unmounted = false;
componentDidActivate.call(this);
}
};
Component.prototype.componentWillUnactivate = function () {
componentWillUnactivate.call(this);
unmount.call(this);
};
Component.prototype.componentWillUnmount = function () {
// Because we will manually call the componentWillUnmount lifecycle
// so we need to prevent it from firing multiple times
if (!this._unmounted) {
this._unmounted = true;
componentWillUnmount.call(this);
unmount.call(this);
}
};
function mount() {
const {
_container: {
cache,
identification,
storeElement,
setLifecycle,
},
} = this.props;
const {renderElement} = cache[identification];
setLifecycle(LIFECYCLE.UPDATING);
changePositionByComment(identification, renderElement, storeElement);
}
function unmount() {
const {
_container: {
identification,
storeElement,
cache,
setLifecycle,
},
} = this.props;
const {renderElement, ifStillActivate, reactivate} = cache[identification];
setLifecycle(LIFECYCLE.UNMOUNTED);
changePositionByComment(identification, storeElement, renderElement);
if (ifStillActivate) {
reactivate();
}
}
@withKeepAliveConsumer
class TriggerLifecycleContainer extends React.PureComponent {
identification = null;
ref = null;
activated = false;
ifStillActivate = false;
// Let the lifecycle of the cached component be called normally.
needActivate = true;
lifecycle = LIFECYCLE.MOUNTED;
componentDidMount() {
if (!this.ifStillActivate) {
this.activate();
}
const {eventEmitter, keepAlive} = this.props;
if (keepAlive) {
this.needActivate = true;
eventEmitter.emit([this.identification, COMMAND.ACTIVATE]);
}
}
componentDidCatch() {
if (!this.activated) {
this.activate();
}
}
componentWillUnmount() {
const {eventEmitter, isExisted, getCombinedKeepAlive} = this.props;
const keepAlive = getCombinedKeepAlive();
if (!keepAlive || !isExisted()) {
if (this.ref) {
this.ref.componentWillUnmount();
}
eventEmitter.emit([this.identification, COMMAND.UNMOUNT]);
}
// When the Provider components are unmounted, the cache is not needed,
// so you don't have to execute the componentWillUnactivate lifecycle.
if (keepAlive && isExisted()) {
if (this.ref) {
this.ref.componentWillUnactivate();
}
eventEmitter.emit([this.identification, COMMAND.UNACTIVATE]);
}
}
activate = () => {
this.activated = true;
};
reactivate = () => {
this.ifStillActivate = false;
this.forceUpdate();
};
isNeedActivate = () => {
return this.needActivate;
};
notNeedActivate = () => {
this.needActivate = false;
};
getLifecycle = () => {
return this.lifecycle;
};
setLifecycle = lifecycle => {
this.lifecycle = lifecycle;
};
setRef = ref => {
this.ref = ref;
const {
forwardedRef,
} = this.props;
if (forwardedRef) {
forwardedRef(ref);
}
};
render() {
if (!this.identification) {
// We need to generate a corresponding unique identifier based on the information of the component.
const {providerIdentification, cache} = this.props;
const {
paths,
globalKey,
typeNames,
} = getContextIdentificationByFiberNode(this._reactInternalFiber);
this.identification = md5(
`${providerIdentification}${displayName}${globalKey ? `${globalKey}${typeNames}` : paths}`,
);
// The last activated component must be unactivated before it can be activated again.
const currentCache = cache[this.identification];
if (currentCache) {
this.ifStillActivate = currentCache.activated;
currentCache.ifStillActivate = this.ifStillActivate;
currentCache.reactivate = this.reactivate;
}
}
const {
isNeedActivate,
notNeedActivate,
activated,
getLifecycle,
setLifecycle,
setRef,
identification,
ifStillActivate,
} = this;
const {
eventEmitter,
keepAlive,
unactivate,
setCache,
forwardedRef,
isExisted,
storeElement,
cache,
providerIdentification,
...wrapperProps
} = this.props;
return !ifStillActivate
? (
<Consumer
identification={identification}
keepAlive={keepAlive}
>
<IdentificationContext.Provider
value={{
_identification: identification,
_eventEmitter: eventEmitter,
_keepAlive: keepAlive,
_activated: activated,
_getLifecycle: getLifecycle,
_isExisted: isExisted,
}}
>
<Component
{...wrapperProps}
keepAlive={keepAlive}
ref={setRef}
_container={{
isNeedActivate,
notNeedActivate,
setLifecycle,
eventEmitter,
identification,
storeElement,
cache,
}}
/>
</IdentificationContext.Provider>
</Consumer>
)
: null;
}
}
@withIdentificationConsumer
class ListenUpperKeepAliveContainer extends React.PureComponent {
combinedKeepAlive = this.props.keepAlive;
state = {
activated: true,
};
componentDidMount() {
this.listenUpperKeepAlive();
}
componentWillUnmount() {
this.unlistenUpperKeepAlive();
}
listenUpperKeepAlive() {
const {_identification, _eventEmitter} = this.props;
if (!_identification) {
return;
}
_eventEmitter.on(
[_identification, COMMAND.ACTIVATE],
this.activate = () => this.setState({activated: true}),
true,
);
_eventEmitter.on(
[_identification, COMMAND.UNACTIVATE],
this.unactivate = () => this.setState({activated: false}),
true,
);
_eventEmitter.on(
[_identification, COMMAND.UNMOUNT],
this.unmount = () => this.setState({activated: false}),
true,
);
}
unlistenUpperKeepAlive() {
const {_identification, _eventEmitter} = this.props;
if (!_identification) {
return;
}
_eventEmitter.off([_identification, COMMAND.ACTIVATE], this.activate);
_eventEmitter.off([_identification, COMMAND.UNACTIVATE], this.unactivate);
_eventEmitter.off([_identification, COMMAND.UNMOUNT], this.unmount);
}
getCombinedKeepAlive = () => {
return this.combinedKeepAlive;
};
render() {
const {
_identification,
_eventEmitter,
_keepAlive,
_activated,
_getLifecycle,
keepAlive,
...wrapperProps
} = this.props;
const {activated} = this.state;
// When the parent KeepAlive component is mounted or unmounted,
// use the keepAlive prop of the parent KeepAlive component.
const newKeepAlive = getKeepAlive(keepAlive);
this.combinedKeepAlive = _getLifecycle === undefined || _getLifecycle() === LIFECYCLE.UPDATING
? newKeepAlive
: _identification
? _keepAlive && newKeepAlive
: newKeepAlive;
return activated
? (
<TriggerLifecycleContainer
{...wrapperProps}
keepAlive={this.combinedKeepAlive}
getCombinedKeepAlive={this.getCombinedKeepAlive}
/>
)
: null;
}
}
let NewComponent = ListenUpperKeepAliveContainer;
if (forwardRef) {
NewComponent = React.forwardRef((props, ref) => (
<ListenUpperKeepAliveContainer
{...props}
forwardedRef={ref}
/>
));
}
NewComponent.displayName = `keepAlive(${displayName})`;
return hoistNonReactStatics(NewComponent, Component);
};
}
\ No newline at end of file
import React from 'react';
import hoistNonReactStatics from 'hoist-non-react-statics';
import noop from './noop';
import {COMMAND} from './keepAlive';
import withIdentificationContextConsumer from './withIdentificationContextConsumer';
import getDisplayName from './getDisplayName';
export default function bindLifecycle(Component) {
const {
componentDidMount = noop,
componentDidUpdate = noop,
componentDidActivate = noop,
componentWillUnactivate = noop,
componentWillUnmount = noop,
} = Component.prototype;
Component.prototype.componentDidMount = function () {
componentDidMount.call(this);
this._needActivate = false;
const {
_container: {
identification,
eventEmitter,
activated,
},
keepAlive,
} = this.props;
// Determine whether to execute the componentDidActivate life cycle of the current component based on the activation state of the KeepAlive components
if (!activated && keepAlive !== false) {
componentDidActivate.call(this);
}
eventEmitter.on(
[identification, COMMAND.ACTIVATE],
this._bindActivate = () => this._needActivate = true,
true,
);
eventEmitter.on(
[identification, COMMAND.UNACTIVATE],
this._bindUnactivate = () => {
componentWillUnactivate.call(this);
this._unmounted = false;
},
true,
);
eventEmitter.on(
[identification, COMMAND.UNMOUNT],
this._bindUnmount = () => {
componentWillUnmount.call(this);
this._unmounted = true;
},
true,
);
};
Component.prototype.componentDidUpdate = function () {
componentDidUpdate.call(this);
if (this._needActivate) {
this._needActivate = false;
componentDidActivate.call(this);
}
};
Component.prototype.componentWillUnmount = function () {
if (!this._unmounted) {
componentWillUnmount.call(this);
}
const {
_container: {
identification,
eventEmitter,
},
} = this.props;
eventEmitter.off(
[identification, COMMAND.ACTIVATE],
this._bindActivate,
);
eventEmitter.off(
[identification, COMMAND.UNACTIVATE],
this._bindUnactivate,
);
eventEmitter.off(
[identification, COMMAND.UNMOUNT],
this._bindUnmount,
);
};
const NewComponent = withIdentificationContextConsumer(
({
forwardRef,
_identificationContextProps: {
identification,
eventEmitter,
activated,
keepAlive,
},
...wrapperProps
}) => (
identification
? (
<Component
{...wrapperProps}
keepAlive={keepAlive}
ref={forwardRef || noop}
_container={{
identification,
eventEmitter,
activated,
}}
/>
)
: null
),
);
NewComponent.displayName = `bindLifecycle(${getDisplayName(Component)})`;
return hoistNonReactStatics(
React.forwardRef((props, ref) => (
<NewComponent {...props} forwardRef={ref} />
)),
Component,
);
};
const NODE_TYPES = {
ELEMENT: 1,
COMMENT: 8,
};
function findElementsBetweenComments(node, identification) {
const elements = [];
const childNodes = node.childNodes;
let startCommentExist = false;
for (let i = 0; i < childNodes.length; i++) {
const child = childNodes[i];
if (
child.nodeType === NODE_TYPES.COMMENT &&
child.nodeValue.trim() === identification &&
!startCommentExist
) {
startCommentExist = true;
} else if (startCommentExist && child.nodeType === NODE_TYPES.ELEMENT) {
elements.push(child);
} else if (child.nodeType === NODE_TYPES.COMMENT && startCommentExist) {
return elements;
}
}
return elements;
}
function findComment(node, identification) {
const childNodes = node.childNodes;
for (let i = 0; i < childNodes.length; i++) {
const child = childNodes[i];
if (
child.nodeType === NODE_TYPES.COMMENT &&
child.nodeValue.trim() === identification
) {
return child;
}
}
}
export default function changePositionByComment(identification, presentParentNode, originalParentNode) {
if (!presentParentNode || !originalParentNode) {
return;
}
const elementNodes = findElementsBetweenComments(originalParentNode, identification);
const commentNode = findComment(presentParentNode, identification);
if (!elementNodes.length || !commentNode) {
return;
}
elementNodes.push(elementNodes[elementNodes.length - 1].nextSibling);
elementNodes.unshift(elementNodes[0].previousSibling);
// Deleting comment elements when using commet components will result in component uninstallation errors
for (let i = elementNodes.length - 1; i >= 0; i--) {
presentParentNode.insertBefore(elementNodes[i], commentNode);
}
originalParentNode.appendChild(commentNode);
}
\ No newline at end of file
export default function createEventEmitter() {
let events = Object.create(null);
function on(eventNames, listener, direction = false) {
eventNames = getEventNames(eventNames);
let current = events;
const maxIndex = eventNames.length - 1;
for (let i = 0; i < eventNames.length; i++) {
const key = eventNames[i];
if (!current[key]) {
current[key] = i === maxIndex ? [] : {};
};
current = current[key];
}
if (!Array.isArray(current)) {
throw new Error('Access path error.');
}
if (direction) {
current.unshift(listener);
} else {
current.push(listener);
}
}
function off(eventNames, listener) {
const listeners = getListeners(eventNames);
if (!listeners) {
return;
}
const matchIndex = listeners.findIndex(v => v === listener);
if (matchIndex !== -1) {
listeners.splice(matchIndex, 1);
}
}
function removeAllListeners(eventNames) {
const listeners = getListeners(eventNames);
if (!listeners) {
return;
}
eventNames = getEventNames(eventNames);
const lastEventName = eventNames.pop();
const event = eventNames.reduce((obj, key) => obj[key], events);
event[lastEventName] = [];
}
function emit(eventNames, ...args) {
const listeners = getListeners(eventNames);
if (!listeners) {
return;
}
for (let i = 0; i < listeners.length; i++) {
if (listeners[i]) {
listeners[i](...args);
}
}
}
function listenerCount(eventNames) {
const listeners = getListeners(eventNames);
return listeners ? listeners.length : 0;
}
function clear() {
events = Object.create(null);
}
function getListeners(eventNames) {
eventNames = getEventNames(eventNames);
try {
return eventNames.reduce((obj, key) => obj[key], events);
} catch (e) {}
}
function getEventNames(eventNames) {
if (!eventNames) {
throw new Error('Must exist event name.');
}
if (typeof eventNames === 'string') {
eventNames = [eventNames];
}
return eventNames;
}
return {
on,
off,
emit,
clear,
listenerCount,
removeAllListeners,
};
};
\ No newline at end of file
import {prefix} from './createUniqueIdentification';
export default function createStoreElement() {
const keepAliveDOM = document.createElement('div');
keepAliveDOM.dataset.type = prefix;
keepAliveDOM.style.display = 'none';
document.body.appendChild(keepAliveDOM);
return keepAliveDOM;
}
\ No newline at end of file
const hexDigits = '0123456789abcdef';
export const prefix = 'keep-alive';
/**
* Create UUID
* Reference: https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
* @export
* @returns
*/
export default function createUniqueIdentification(length = 6) {
const strings = [];
for (var i = 0; i < length; i++) {
strings[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
}
return `${prefix}-${strings.join('')}`;
}
\ No newline at end of file
export default function findDOMNodeByFiberNode(fiberNode) {
if (!fiberNode) {
return null;
}
const {
stateNode,
return: parent,
} = fiberNode;
if (!parent) {
return stateNode && stateNode.containerInfo;
}
if (stateNode && stateNode.nodeType) {
return stateNode;
}
return findDOMNodeByFiberNode(parent);
}
\ No newline at end of file
import {keepAliveProviderTypeName} from '../components/Provider';
export default function getContextIdentificationByFiberNode(fiberNode) {
let globalKey = null;
let typeNames = '';
function getPathsByFiberNode(fiberNode) {
if (!fiberNode) {
return '';
}
const {
type,
key,
index,
} = fiberNode;
let typeName = type && type.name ? type.name : '';
if (typeName === keepAliveProviderTypeName) {
return '';
}
const joinName = getPathsByFiberNode(fiberNode.return);
if (type && type.displayName && type.displayName.indexOf('keepAlive') !== -1) {
if (!globalKey) {
globalKey = key;
}
}
typeNames += typeName;
return `${key || index}${joinName}`;
}
const paths = getPathsByFiberNode(fiberNode);
return {
paths,
globalKey,
typeNames,
};
}
\ No newline at end of file
export default function getDisplayName(Component) {
return Component.displayName || Component.name || null;
}
\ No newline at end of file
export default function getKeepAlive(keepAlive) {
return keepAlive === undefined ? true : keepAlive;
}
\ No newline at end of file
import React from 'react';
import hoistNonReactStatics from 'hoist-non-react-statics';
import IdentificationContext from '../contexts/IdentificationContext';
import Consumer, {LIFECYCLE} from '../components/Consumer';
import {START_MOUNTING_DOM} from '../components/Provider';
import md5 from './md5';
import noop from './noop';
import getContextIdentificationByFiberNode from './getContextIdentificationByFiberNode';
import withIdentificationConsumer from './withIdentificationConsumer';
import withKeepAliveConsumer from './withKeepAliveConsumer';
import changePositionByComment from './changePositionByComment';
import getDisplayName from './getDisplayName';
import getKeepAlive from './getKeepAlive';
export const COMMAND = {
UNACTIVATE: 'unactivate',
UNMOUNT: 'unmount',
ACTIVATE: 'activate',
};
export default function keepAlive({
name,
forwardRef = false,
} = {}) {
return Component => {
const {
componentDidMount = noop,
componentDidUpdate = noop,
componentDidActivate = noop,
componentWillUnactivate = noop,
componentWillUnmount = noop,
} = Component.prototype;
const displayName = name || getDisplayName(Component);
if (!displayName) {
throw new Error('Each component must have a name, which can be the component\'s displayName or name static property. You can also configure name when keepAlive decorates the component.');
}
Component.prototype.componentDidMount = function () {
const {
_container,
keepAlive,
} = this.props;
const {
notNeedActivate,
identification,
eventEmitter,
} = _container;
notNeedActivate();
const cb = () => {
mount.call(this);
eventEmitter.off([identification, START_MOUNTING_DOM], cb);
};
eventEmitter.on([identification, START_MOUNTING_DOM], cb);
componentDidMount.call(this);
if (keepAlive) {
componentDidActivate.call(this);
}
};
Component.prototype.componentDidUpdate = function () {
componentDidUpdate.call(this);
const {
_container,
} = this.props;
const {
notNeedActivate,
isNeedActivate,
} = _container;
if (isNeedActivate()) {
notNeedActivate();
mount.call(this);
this._unmounted = false;
componentDidActivate.call(this);
}
};
Component.prototype.componentWillUnactivate = function () {
componentWillUnactivate.call(this);
unmount.call(this);
};
Component.prototype.componentWillUnmount = function () {
// Because we will manually call the componentWillUnmount lifecycle
// so we need to prevent it from firing multiple times
if (!this._unmounted) {
this._unmounted = true;
componentWillUnmount.call(this);
unmount.call(this);
}
};
function mount() {
const {
_container: {
cache,
identification,
storeElement,
setLifecycle,
},
} = this.props;
const {renderElement} = cache[identification];
setLifecycle(LIFECYCLE.UPDATING);
changePositionByComment(identification, renderElement, storeElement);
}
function unmount() {
const {
_container: {
identification,
storeElement,
cache,
setLifecycle,
},
} = this.props;
const {renderElement, ifStillActivate, reactivate} = cache[identification];
setLifecycle(LIFECYCLE.UNMOUNTED);
changePositionByComment(identification, storeElement, renderElement);
if (ifStillActivate) {
reactivate();
}
}
class TriggerLifecycleContainer extends React.PureComponent {
identification = null;
ref = null;
activated = false;
ifStillActivate = false;
// Let the lifecycle of the cached component be called normally.
needActivate = true;
lifecycle = LIFECYCLE.MOUNTED;
componentDidMount() {
if (!this.ifStillActivate) {
this.activate();
}
const {
keepAlive,
_keepAliveContextProps: {
eventEmitter,
},
} = this.props;
if (keepAlive) {
this.needActivate = true;
eventEmitter.emit([this.identification, COMMAND.ACTIVATE]);
}
}
componentDidCatch() {
if (!this.activated) {
this.activate();
}
}
componentWillUnmount() {
const {
getCombinedKeepAlive,
_keepAliveContextProps: {
eventEmitter,
isExisted,
},
} = this.props;
const keepAlive = getCombinedKeepAlive();
if (!keepAlive || !isExisted()) {
if (this.ref) {
this.ref.componentWillUnmount();
}
eventEmitter.emit([this.identification, COMMAND.UNMOUNT]);
}
// When the Provider components are unmounted, the cache is not needed,
// so you don't have to execute the componentWillUnactivate lifecycle.
if (keepAlive && isExisted()) {
if (this.ref) {
this.ref.componentWillUnactivate();
}
eventEmitter.emit([this.identification, COMMAND.UNACTIVATE]);
}
}
activate = () => {
this.activated = true;
};
reactivate = () => {
this.ifStillActivate = false;
this.forceUpdate();
};
isNeedActivate = () => {
return this.needActivate;
};
notNeedActivate = () => {
this.needActivate = false;
};
getLifecycle = () => {
return this.lifecycle;
};
setLifecycle = lifecycle => {
this.lifecycle = lifecycle;
};
setRef = ref => {
this.ref = ref;
const {
forwardedRef,
} = this.props;
if (forwardedRef) {
forwardedRef(ref);
}
};
render() {
if (!this.identification) {
// We need to generate a corresponding unique identifier based on the information of the component.
const {providerIdentification, cache} = this.props._keepAliveContextProps;
const {
paths,
globalKey,
typeNames,
} = getContextIdentificationByFiberNode(this._reactInternalFiber);
this.identification = md5(
`${providerIdentification}${displayName}${globalKey ? `${globalKey}${typeNames}` : paths}`,
);
// The last activated component must be unactivated before it can be activated again.
const currentCache = cache[this.identification];
if (currentCache) {
this.ifStillActivate = currentCache.activated;
currentCache.ifStillActivate = this.ifStillActivate;
currentCache.reactivate = this.reactivate;
}
}
const {
isNeedActivate,
notNeedActivate,
activated,
getLifecycle,
setLifecycle,
setRef,
identification,
ifStillActivate,
} = this;
const {
keepAlive,
forwardedRef,
_keepAliveContextProps: {
isExisted,
storeElement,
cache,
eventEmitter,
},
...wrapperProps
} = this.props;
return !ifStillActivate
? (
<Consumer
identification={identification}
keepAlive={keepAlive}
>
<IdentificationContext.Provider
value={{
identification,
eventEmitter,
keepAlive,
activated,
getLifecycle,
isExisted,
}}
>
<Component
{...wrapperProps}
keepAlive={keepAlive}
ref={setRef}
_container={{
isNeedActivate,
notNeedActivate,
setLifecycle,
eventEmitter,
identification,
storeElement,
cache,
}}
/>
</IdentificationContext.Provider>
</Consumer>
)
: null;
}
}
@withIdentificationConsumer
@withKeepAliveConsumer
class ListenUpperKeepAliveContainer extends React.PureComponent {
combinedKeepAlive = this.props.keepAlive;
state = {
activated: true,
};
componentDidMount() {
this.listenUpperKeepAlive();
}
componentWillUnmount() {
this.unlistenUpperKeepAlive();
}
listenUpperKeepAlive() {
const {identification, eventEmitter} = this.props._identificationContextProps;
if (!identification) {
return;
}
eventEmitter.on(
[identification, COMMAND.ACTIVATE],
this.activate = () => this.setState({activated: true}),
true,
);
eventEmitter.on(
[identification, COMMAND.UNACTIVATE],
this.unactivate = () => this.setState({activated: false}),
true,
);
eventEmitter.on(
[identification, COMMAND.UNMOUNT],
this.unmount = () => this.setState({activated: false}),
true,
);
}
unlistenUpperKeepAlive() {
const {identification, eventEmitter} = this.props._identificationContextProps;
if (!identification) {
return;
}
eventEmitter.off([identification, COMMAND.ACTIVATE], this.activate);
eventEmitter.off([identification, COMMAND.UNACTIVATE], this.unactivate);
eventEmitter.off([identification, COMMAND.UNMOUNT], this.unmount);
}
getCombinedKeepAlive = () => {
return this.combinedKeepAlive;
};
render() {
const {
_identificationContextProps: {
identification,
keepAlive: _keepAlive,
getLifecycle,
},
keepAlive,
...wrapperProps
} = this.props;
const {activated} = this.state;
// When the parent KeepAlive component is mounted or unmounted,
// use the keepAlive prop of the parent KeepAlive component.
const newKeepAlive = getKeepAlive(keepAlive);
this.combinedKeepAlive = getLifecycle === undefined || getLifecycle() === LIFECYCLE.UPDATING
? newKeepAlive
: identification
? _keepAlive && newKeepAlive
: newKeepAlive;
return activated
? (
<TriggerLifecycleContainer
{...wrapperProps}
keepAlive={this.combinedKeepAlive}
getCombinedKeepAlive={this.getCombinedKeepAlive}
/>
)
: null;
}
}
let NewComponent = ListenUpperKeepAliveContainer;
if (forwardRef) {
NewComponent = React.forwardRef((props, ref) => (
<ListenUpperKeepAliveContainer
{...props}
forwardedRef={ref}
/>
));
}
NewComponent.displayName = `keepAlive(${displayName})`;
return hoistNonReactStatics(NewComponent, Component);
};
}
\ No newline at end of file
import React from 'react';
import hoistNonReactStatics from 'hoist-non-react-statics';
import IdentificationContext from '../contexts/IdentificationContext';
import Consumer, {LIFECYCLE} from '../components/Consumer';
import {START_MOUNTING_DOM} from '../components/Provider';
import md5 from './md5';
import noop from './noop';
import getContextIdentificationByFiberNode from './getContextIdentificationByFiberNode';
import withIdentificationContextConsumer from './withIdentificationContextConsumer';
import withKeepAliveContextConsumer from './withKeepAliveContextConsumer';
import changePositionByComment from './changePositionByComment';
import shallowEqual from './shallowEqual';
import getDisplayName from './getDisplayName';
import getKeepAlive from './getKeepAlive';
export const COMMAND = {
UNACTIVATE: 'unactivate',
UNMOUNT: 'unmount',
ACTIVATE: 'activate',
};
export default function keepAlive({
name,
forwardRef = false,
} = {}) {
return Component => {
const {
componentDidMount = noop,
componentDidUpdate = noop,
componentDidActivate = noop,
componentWillUnactivate = noop,
componentWillUnmount = noop,
} = Component.prototype;
const displayName = name || getDisplayName(Component);
if (!displayName) {
throw new Error('Each component must have a name, which can be the component\'s displayName or name static property. You can also configure name when keepAlive decorates the component.');
}
Component.prototype.componentDidMount = function () {
const {
_container,
keepAlive,
} = this.props;
const {
notNeedActivate,
identification,
eventEmitter,
} = _container;
notNeedActivate();
const cb = () => {
mount.call(this);
eventEmitter.off([identification, START_MOUNTING_DOM], cb);
};
eventEmitter.on([identification, START_MOUNTING_DOM], cb);
componentDidMount.call(this);
if (keepAlive) {
componentDidActivate.call(this);
}
};
Component.prototype.componentDidUpdate = function () {
componentDidUpdate.call(this);
const {
_container,
} = this.props;
const {
notNeedActivate,
isNeedActivate,
} = _container;
if (isNeedActivate()) {
notNeedActivate();
mount.call(this);
this._unmounted = false;
componentDidActivate.call(this);
}
};
Component.prototype.componentWillUnactivate = function () {
componentWillUnactivate.call(this);
unmount.call(this);
};
Component.prototype.componentWillUnmount = function () {
// Because we will manually call the componentWillUnmount lifecycle
// so we need to prevent it from firing multiple times
if (!this._unmounted) {
this._unmounted = true;
componentWillUnmount.call(this);
unmount.call(this);
}
};
function mount() {
const {
_container: {
cache,
identification,
storeElement,
setLifecycle,
},
} = this.props;
const {renderElement} = cache[identification];
setLifecycle(LIFECYCLE.UPDATING);
changePositionByComment(identification, renderElement, storeElement);
}
function unmount() {
const {
_container: {
identification,
storeElement,
cache,
setLifecycle,
},
} = this.props;
const {renderElement, ifStillActivate, reactivate} = cache[identification];
setLifecycle(LIFECYCLE.UNMOUNTED);
changePositionByComment(identification, storeElement, renderElement);
if (ifStillActivate) {
reactivate();
}
}
class TriggerLifecycleContainer extends React.PureComponent {
identification = null;
ref = null;
activated = false;
ifStillActivate = false;
// Let the lifecycle of the cached component be called normally.
needActivate = true;
lifecycle = LIFECYCLE.MOUNTED;
componentDidMount() {
if (!this.ifStillActivate) {
this.activate();
}
const {
keepAlive,
_keepAliveContextProps: {
eventEmitter,
},
} = this.props;
if (keepAlive) {
this.needActivate = true;
eventEmitter.emit([this.identification, COMMAND.ACTIVATE]);
}
}
componentDidCatch() {
if (!this.activated) {
this.activate();
}
}
componentWillUnmount() {
const {
getCombinedKeepAlive,
_keepAliveContextProps: {
eventEmitter,
isExisted,
},
} = this.props;
const keepAlive = getCombinedKeepAlive();
if (!keepAlive || !isExisted()) {
if (this.ref) {
this.ref.componentWillUnmount();
}
eventEmitter.emit([this.identification, COMMAND.UNMOUNT]);
}
// When the Provider components are unmounted, the cache is not needed,
// so you don't have to execute the componentWillUnactivate lifecycle.
if (keepAlive && isExisted()) {
if (this.ref) {
this.ref.componentWillUnactivate();
}
eventEmitter.emit([this.identification, COMMAND.UNACTIVATE]);
}
}
activate = () => {
this.activated = true;
};
reactivate = () => {
this.ifStillActivate = false;
this.forceUpdate();
};
isNeedActivate = () => {
return this.needActivate;
};
notNeedActivate = () => {
this.needActivate = false;
};
getLifecycle = () => {
return this.lifecycle;
};
setLifecycle = lifecycle => {
this.lifecycle = lifecycle;
};
setRef = ref => {
this.ref = ref;
const {
forwardedRef,
} = this.props;
if (forwardedRef) {
forwardedRef(ref);
}
};
render() {
if (!this.identification) {
// We need to generate a corresponding unique identifier based on the information of the component.
const {providerIdentification, cache} = this.props._keepAliveContextProps;
const {
paths,
globalKey,
typeNames,
} = getContextIdentificationByFiberNode(this._reactInternalFiber);
this.identification = md5(
`${providerIdentification}${displayName}${globalKey ? `${globalKey}${typeNames}` : paths}`,
);
// The last activated component must be unactivated before it can be activated again.
const currentCache = cache[this.identification];
if (currentCache) {
this.ifStillActivate = currentCache.activated;
currentCache.ifStillActivate = this.ifStillActivate;
currentCache.reactivate = this.reactivate;
}
}
const {
isNeedActivate,
notNeedActivate,
activated,
getLifecycle,
setLifecycle,
setRef,
identification,
ifStillActivate,
} = this;
const {
keepAlive,
forwardedRef,
_keepAliveContextProps: {
isExisted,
storeElement,
cache,
eventEmitter,
setCache,
unactivate,
},
...wrapperProps
} = this.props;
return !ifStillActivate
? (
<Consumer
identification={identification}
keepAlive={keepAlive}
cache={cache}
setCache={setCache}
unactivate={unactivate}
>
<IdentificationContext.Provider
value={{
identification,
eventEmitter,
keepAlive,
activated,
getLifecycle,
isExisted,
}}
>
<Component
{...wrapperProps}
keepAlive={keepAlive}
ref={setRef}
_container={{
isNeedActivate,
notNeedActivate,
setLifecycle,
eventEmitter,
identification,
storeElement,
cache,
}}
/>
</IdentificationContext.Provider>
</Consumer>
)
: null;
}
}
@withKeepAliveContextConsumer
@withIdentificationContextConsumer
class ListenUpperKeepAliveContainer extends React.Component {
combinedKeepAlive = this.props.keepAlive;
state = {
activated: true,
};
shouldComponentUpdate(nextProps, nextState) {
if (this.state.activated !== nextState.activated) {
return true;
}
const {
_keepAliveContextProps,
_identificationContextProps,
...rest
} = this.props;
const {
_keepAliveContextProps: _nextKeepAliveContextProps,
_identificationContextProps: _nextIdentificationContextProps,
...nextRest
} = nextProps;
if (!shallowEqual(rest, nextRest)) {
return true;
}
if (
!shallowEqual(_keepAliveContextProps, _nextKeepAliveContextProps) ||
!shallowEqual(_identificationContextProps, _nextIdentificationContextProps)
) {
return true;
}
return false;
}
componentDidMount() {
this.listenUpperKeepAlive();
}
componentWillUnmount() {
this.unlistenUpperKeepAlive();
}
listenUpperKeepAlive() {
const {identification, eventEmitter} = this.props._identificationContextProps;
if (!identification) {
return;
}
eventEmitter.on(
[identification, COMMAND.ACTIVATE],
this.activate = () => this.setState({activated: true}),
true,
);
eventEmitter.on(
[identification, COMMAND.UNACTIVATE],
this.unactivate = () => this.setState({activated: false}),
true,
);
eventEmitter.on(
[identification, COMMAND.UNMOUNT],
this.unmount = () => this.setState({activated: false}),
true,
);
}
unlistenUpperKeepAlive() {
const {identification, eventEmitter} = this.props._identificationContextProps;
if (!identification) {
return;
}
eventEmitter.off([identification, COMMAND.ACTIVATE], this.activate);
eventEmitter.off([identification, COMMAND.UNACTIVATE], this.unactivate);
eventEmitter.off([identification, COMMAND.UNMOUNT], this.unmount);
}
getCombinedKeepAlive = () => {
return this.combinedKeepAlive;
};
render() {
const {
_identificationContextProps: {
identification,
keepAlive: _keepAlive,
getLifecycle,
},
keepAlive,
...wrapperProps
} = this.props;
const {activated} = this.state;
// When the parent KeepAlive component is mounted or unmounted,
// use the keepAlive prop of the parent KeepAlive component.
const newKeepAlive = getKeepAlive(keepAlive);
this.combinedKeepAlive = getLifecycle === undefined || getLifecycle() === LIFECYCLE.UPDATING
? newKeepAlive
: identification
? _keepAlive && newKeepAlive
: newKeepAlive;
return activated
? (
<TriggerLifecycleContainer
{...wrapperProps}
keepAlive={this.combinedKeepAlive}
getCombinedKeepAlive={this.getCombinedKeepAlive}
/>
)
: null;
}
}
let NewComponent = ListenUpperKeepAliveContainer;
if (forwardRef) {
NewComponent = React.forwardRef((props, ref) => (
<ListenUpperKeepAliveContainer
{...props}
forwardedRef={ref}
/>
));
}
NewComponent.displayName = `keepAlive(${displayName})`;
return hoistNonReactStatics(NewComponent, Component);
};
}
\ No newline at end of file
import md5 from 'js-md5';
import {prefix} from './createUniqueIdentification';
export default function createMD5(string = '', length = 6) {
return `${prefix}-${md5(string).substr(0, length)}`;
}
\ No newline at end of file
export default function noop() {};
\ No newline at end of file
/**
* From react
*/
function is(x, y) {
return (
(x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
);
}
const hasOwnProperty = Object.prototype.hasOwnProperty;
function shallowEqual(objA, objB) {
if (is(objA, objB)) {
return true;
}
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
// Test for A's keys different from B.
for (let i = 0; i < keysA.length; i++) {
if (
!hasOwnProperty.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])
) {
return false;
}
}
return true;
}
export default shallowEqual;
\ No newline at end of file
import React from 'react';
import IdentificationContext from '../contexts/IdentificationContext';
import getDisplayName from './getDisplayName';
export default function withIdentificationContextConsumer(Component) {
const NewComponent = props => (
<IdentificationContext.Consumer>
{contextProps => <Component _identificationContextProps={contextProps || {}} {...props} />}
</IdentificationContext.Consumer>
);
NewComponent.displayName = `withIdentificationContextConsumer(${getDisplayName(Component)})`;
return NewComponent;
}
\ No newline at end of file
import React from 'react';
import KeepAliveContext from '../contexts/KeepAliveContext';
import getDisplayName from './getDisplayName';
export default function withKeepAliveContextConsumer(Component) {
const NewComponent = (props, ref) => (
<KeepAliveContext.Consumer>
{contextProps => <Component _keepAliveContextProps={contextProps || {}} {...props} ref={ref} />}
</KeepAliveContext.Consumer>
);
NewComponent.displayName = `withKeepAliveContextConsumer(${getDisplayName(Component)})`;
return React.forwardRef(NewComponent);
}
\ No newline at end of file
import {configure} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({
adapter: new Adapter(),
});
{
"compilerOptions": {
"baseUrl": ".",
"outDir": "es",
"module": "esnext",
"esModuleInterop": true,
"target": "es5",
"lib": ["es6", "dom"],
"allowJs": true,
"sourceMap": false,
"jsx": "react",
"moduleResolution": "node",
"rootDir": "src",
"forceConsistentCasingInFileNames": true,
"noImplicitThis": true,
"noImplicitAny": true,
"strictNullChecks": true,
"suppressImplicitAnyIndexErrors": true,
"experimentalDecorators": true,
"noUnusedLocals": true,
"typeRoots": [
"node_modules/@types",
"typings",
],
},
"exclude": [
"node_modules",
"jest",
]
}
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended"
],
"rules": {
"max-line-length": false,
"no-console": false,
"quotemark": [true, "single", "jsx-double"],
"trailing-comma": [true, {"multiline": "ignore", "singleline": "never"}],
"ordered-imports": false,
"member-ordering": false,
"max-classes-per-file": false,
"no-unsafe-finally": false,
"object-literal-sort-keys": false,
"arrow-parens": [true, "ban-single-arg-parens"]
}
}
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
const webpack = require('webpack');
const ROOT = path.join(__dirname, 'demo');
const SRC = path.join(ROOT, 'src');
module.exports = {
mode: 'development',
devtool: 'source-map',
entry: {
index: path.join(SRC, 'index.js'),
},
output: {
path: path.join(ROOT, 'build'),
filename: 'static/[name].js',
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
inject: true,
template: path.join(ROOT, 'index.html'),
}),
],
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
include: SRC,
},
],
},
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment