({} as any);
7 |
8 | export default KeepAliveContext;
9 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import Provider from './components/Provider';
2 | import KeepAlive from './components/KeepAlive';
3 | import bindLifecycle from './utils/bindLifecycle';
4 | import useKeepAliveEffect from './utils/useKeepAliveEffect';
5 |
6 | export {
7 | Provider,
8 | KeepAlive,
9 | bindLifecycle,
10 | useKeepAliveEffect,
11 | };
12 |
--------------------------------------------------------------------------------
/src/utils/bindLifecycle.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import hoistNonReactStatics from 'hoist-non-react-statics';
3 | import noop from './noop';
4 | import {warn} from './debug';
5 | import {COMMAND} from './keepAliveDecorator';
6 | import withIdentificationContextConsumer from './withIdentificationContextConsumer';
7 | import getDisplayName from './getDisplayName';
8 |
9 | export const bindLifecycleTypeName = '$$bindLifecycle';
10 |
11 | export default function bindLifecycle(Component: React.ComponentClass
) {
12 | const WrappedComponent = (Component as any).WrappedComponent || (Component as any).wrappedComponent || Component;
13 |
14 | const {
15 | componentDidMount = noop,
16 | componentDidUpdate = noop,
17 | componentDidActivate = noop,
18 | componentWillUnactivate = noop,
19 | componentWillUnmount = noop,
20 | shouldComponentUpdate = noop,
21 | } = WrappedComponent.prototype;
22 |
23 | WrappedComponent.prototype.componentDidMount = function () {
24 | componentDidMount.call(this);
25 | this._needActivate = false;
26 | const {
27 | _container: {
28 | identification,
29 | eventEmitter,
30 | activated,
31 | },
32 | keepAlive,
33 | } = this.props;
34 | // Determine whether to execute the componentDidActivate life cycle of the current component based on the activation state of the KeepAlive components
35 | if (!activated && keepAlive !== false) {
36 | componentDidActivate.call(this);
37 | }
38 | eventEmitter.on(
39 | [identification, COMMAND.ACTIVATE],
40 | this._bindActivate = () => this._needActivate = true,
41 | true,
42 | );
43 | eventEmitter.on(
44 | [identification, COMMAND.UNACTIVATE],
45 | this._bindUnactivate = () => {
46 | componentWillUnactivate.call(this);
47 | this._unmounted = false;
48 | },
49 | true,
50 | );
51 | eventEmitter.on(
52 | [identification, COMMAND.UNMOUNT],
53 | this._bindUnmount = () => {
54 | componentWillUnmount.call(this);
55 | this._unmounted = true;
56 | },
57 | true,
58 | );
59 | };
60 |
61 | // In order to be able to re-update after transferring the DOM, we need to block the first update.
62 | WrappedComponent.prototype.shouldComponentUpdate = function (...args: any) {
63 | if (this._needActivate) {
64 | this.forceUpdate();
65 | return false;
66 | }
67 | return shouldComponentUpdate.call(this, ...args) || true;
68 | };
69 |
70 | WrappedComponent.prototype.componentDidUpdate = function (...args: any) {
71 | componentDidUpdate.call(this, ...args);
72 | if (this._needActivate) {
73 | this._needActivate = false;
74 | componentDidActivate.call(this);
75 | }
76 | };
77 | WrappedComponent.prototype.componentWillUnmount = function () {
78 | if (!this._unmounted) {
79 | componentWillUnmount.call(this);
80 | }
81 | const {
82 | _container: {
83 | identification,
84 | eventEmitter,
85 | },
86 | } = this.props;
87 | eventEmitter.off(
88 | [identification, COMMAND.ACTIVATE],
89 | this._bindActivate,
90 | );
91 | eventEmitter.off(
92 | [identification, COMMAND.UNACTIVATE],
93 | this._bindUnactivate,
94 | );
95 | eventEmitter.off(
96 | [identification, COMMAND.UNMOUNT],
97 | this._bindUnmount,
98 | );
99 | };
100 |
101 | const BindLifecycleHOC = withIdentificationContextConsumer(
102 | ({
103 | forwardRef,
104 | _identificationContextProps: {
105 | identification,
106 | eventEmitter,
107 | activated,
108 | keepAlive,
109 | extra,
110 | },
111 | ...wrapperProps
112 | }) => {
113 | if (!identification) {
114 | warn('[React Keep Alive] You should not use bindLifecycle outside a .');
115 | return null;
116 | }
117 | return (
118 |
129 | );
130 | },
131 | );
132 | const BindLifecycle = React.forwardRef((props: P, ref) => (
133 |
134 | ));
135 |
136 | (BindLifecycle as any).WrappedComponent = WrappedComponent;
137 | BindLifecycle.displayName = `${bindLifecycleTypeName}(${getDisplayName(Component)})`;
138 | return hoistNonReactStatics(
139 | BindLifecycle,
140 | Component,
141 | ) as any;
142 | }
143 |
--------------------------------------------------------------------------------
/src/utils/changePositionByComment.ts:
--------------------------------------------------------------------------------
1 | enum NODE_TYPES {
2 | ELEMENT = 1,
3 | COMMENT = 8,
4 | }
5 |
6 | function findElementsBetweenComments(node: Node, identification: string): Node[] {
7 | const elements = [];
8 | const childNodes = node.childNodes as any;
9 | let startCommentExist = false;
10 | for (const child of childNodes) {
11 | if (
12 | child.nodeType === NODE_TYPES.COMMENT &&
13 | child.nodeValue.trim() === identification &&
14 | !startCommentExist
15 | ) {
16 | startCommentExist = true;
17 | } else if (startCommentExist && child.nodeType === NODE_TYPES.ELEMENT) {
18 | elements.push(child);
19 | } else if (child.nodeType === NODE_TYPES.COMMENT && startCommentExist) {
20 | return elements;
21 | }
22 | }
23 | return elements;
24 | }
25 |
26 | function findComment(node: Node, identification: string): Node | undefined {
27 | const childNodes = node.childNodes as any;
28 | for (const child of childNodes) {
29 | if (
30 | child.nodeType === NODE_TYPES.COMMENT &&
31 | child.nodeValue.trim() === identification
32 | ) {
33 | return child;
34 | }
35 | }
36 | }
37 |
38 | export default function changePositionByComment(identification: string, presentParentNode: Node, originalParentNode: Node) {
39 | if (!presentParentNode || !originalParentNode) {
40 | return;
41 | }
42 | const elementNodes = findElementsBetweenComments(originalParentNode, identification);
43 | const commentNode = findComment(presentParentNode, identification);
44 | if (!elementNodes.length || !commentNode) {
45 | return;
46 | }
47 | elementNodes.push(elementNodes[elementNodes.length - 1].nextSibling as Node);
48 | elementNodes.unshift(elementNodes[0].previousSibling as Node);
49 | // Deleting comment elements when using commet components will result in component uninstallation errors
50 | for (let i = elementNodes.length - 1; i >= 0; i--) {
51 | presentParentNode.insertBefore(elementNodes[i], commentNode);
52 | }
53 | originalParentNode.appendChild(commentNode);
54 | }
55 |
--------------------------------------------------------------------------------
/src/utils/createEventEmitter.ts:
--------------------------------------------------------------------------------
1 | import {warn} from './debug';
2 |
3 | type EventNames = string | string[];
4 |
5 | type Listener = (...args: any) => void;
6 |
7 | export default function createEventEmitter() {
8 | let events = Object.create(null);
9 |
10 | function on(eventNames: EventNames, listener: Listener, direction = false) {
11 | eventNames = getEventNames(eventNames);
12 | let current = events;
13 | const maxIndex = eventNames.length - 1;
14 | for (let i = 0; i < eventNames.length; i++) {
15 | const key = eventNames[i];
16 | if (!current[key]) {
17 | current[key] = i === maxIndex ? [] : {};
18 | }
19 | current = current[key];
20 | }
21 | if (!Array.isArray(current)) {
22 | warn('[React Keep Alive] Access path error.');
23 | }
24 | if (direction) {
25 | current.unshift(listener);
26 | } else {
27 | current.push(listener);
28 | }
29 | }
30 |
31 | function off(eventNames: EventNames, listener: Listener) {
32 | const listeners = getListeners(eventNames);
33 | if (!listeners) {
34 | return;
35 | }
36 | const matchIndex = listeners.findIndex((v: Listener) => v === listener);
37 | if (matchIndex !== -1) {
38 | listeners.splice(matchIndex, 1);
39 | }
40 | }
41 |
42 | function removeAllListeners(eventNames: EventNames) {
43 | const listeners = getListeners(eventNames);
44 | if (!listeners) {
45 | return;
46 | }
47 | eventNames = getEventNames(eventNames);
48 | const lastEventName = eventNames.pop();
49 | if (lastEventName) {
50 | const event = eventNames.reduce((obj, key) => obj[key], events);
51 | event[lastEventName] = [];
52 | }
53 | }
54 |
55 | function emit(eventNames: EventNames, ...args: any) {
56 | const listeners = getListeners(eventNames);
57 | if (!listeners) {
58 | return;
59 | }
60 | for (const listener of listeners) {
61 | if (listener) {
62 | listener(...args);
63 | }
64 | }
65 | }
66 |
67 | function listenerCount(eventNames: EventNames) {
68 | const listeners = getListeners(eventNames);
69 | return listeners ? listeners.length : 0;
70 | }
71 |
72 | function clear() {
73 | events = Object.create(null);
74 | }
75 |
76 | function getListeners(eventNames: EventNames): Listener[] | undefined {
77 | eventNames = getEventNames(eventNames);
78 | try {
79 | return eventNames.reduce((obj, key) => obj[key], events);
80 | } catch (e) {
81 | return;
82 | }
83 | }
84 |
85 | function getEventNames(eventNames: EventNames): string[] {
86 | if (!eventNames) {
87 | warn('[React Keep Alive] Must exist event name.');
88 | }
89 | if (typeof eventNames === 'string') {
90 | eventNames = [eventNames];
91 | }
92 | return eventNames;
93 | }
94 |
95 | return {
96 | on,
97 | off,
98 | emit,
99 | clear,
100 | listenerCount,
101 | removeAllListeners,
102 | };
103 | }
104 |
--------------------------------------------------------------------------------
/src/utils/createStoreElement.ts:
--------------------------------------------------------------------------------
1 | import {prefix} from './createUniqueIdentification';
2 |
3 | export default function createStoreElement(): HTMLElement {
4 | const keepAliveDOM = document.createElement('div');
5 | keepAliveDOM.dataset.type = prefix;
6 | keepAliveDOM.style.display = 'none';
7 | document.body.appendChild(keepAliveDOM);
8 | return keepAliveDOM;
9 | }
10 |
--------------------------------------------------------------------------------
/src/utils/createUniqueIdentification.ts:
--------------------------------------------------------------------------------
1 | const hexDigits = '0123456789abcdef';
2 |
3 | export const prefix = 'keep-alive';
4 |
5 | /**
6 | * Create UUID
7 | * Reference: https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
8 | * @export
9 | * @returns
10 | */
11 | export default function createUniqueIdentification(length = 6) {
12 | const strings = [];
13 | for (let i = 0; i < length; i++) {
14 | strings[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
15 | }
16 | return `${prefix}-${strings.join('')}`;
17 | }
18 |
--------------------------------------------------------------------------------
/src/utils/debug.ts:
--------------------------------------------------------------------------------
1 | type Warn = (message?: string) => void;
2 |
3 | export let warn: Warn = () => undefined;
4 |
5 | if (process.env.NODE_ENV !== 'production') {
6 | /**
7 | * Prints a warning in the console if it exists.
8 | *
9 | * @param {*} message
10 | */
11 | warn = message => {
12 | if (typeof console !== undefined && typeof console.error === 'function') {
13 | console.error(message);
14 | } else {
15 | throw new Error(message);
16 | }
17 | };
18 | }
19 |
--------------------------------------------------------------------------------
/src/utils/getDisplayName.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | export default function getDisplayName(Component: React.ComponentType) {
4 | return Component.displayName || Component.name || 'Component';
5 | }
6 |
--------------------------------------------------------------------------------
/src/utils/getKeepAlive.ts:
--------------------------------------------------------------------------------
1 | import isRegExp from './isRegExp';
2 |
3 | type Pattern = string | string[] | RegExp;
4 |
5 | function matches (pattern: Pattern, name: string) {
6 | if (Array.isArray(pattern)) {
7 | return pattern.indexOf(name) > -1;
8 | } else if (typeof pattern === 'string') {
9 | return pattern.split(',').indexOf(name) > -1;
10 | } else if (isRegExp(pattern)) {
11 | return pattern.test(name);
12 | }
13 | return false;
14 | }
15 |
16 | export default function getKeepAlive(
17 | name: string,
18 | include?: Pattern,
19 | exclude?: Pattern,
20 | disabled?: boolean
21 | ) {
22 | if (disabled !== undefined) {
23 | return !disabled;
24 | }
25 | if (
26 | (include && (!name || !matches(include, name))) ||
27 | (exclude && name && matches(exclude, name))
28 | ) {
29 | return false;
30 | }
31 | return true;
32 | }
33 |
--------------------------------------------------------------------------------
/src/utils/getKeyByFiberNode.ts:
--------------------------------------------------------------------------------
1 | import {WithKeepAliveContextConsumerDisplayName} from './withKeepAliveContextConsumer';
2 |
3 | export default function getKeyByFiberNode(fiberNode: any): string | null {
4 | if (!fiberNode) {
5 | return null;
6 | }
7 | const {
8 | key,
9 | type,
10 | } = fiberNode;
11 | if (type.displayName && type.displayName.indexOf(WithKeepAliveContextConsumerDisplayName) !== -1) {
12 | return key;
13 | }
14 | return getKeyByFiberNode(fiberNode.return);
15 | }
16 |
--------------------------------------------------------------------------------
/src/utils/isRegExp.ts:
--------------------------------------------------------------------------------
1 | export default function isRegExp(value: RegExp) {
2 | return value && Object.prototype.toString.call(value) === '[object RegExp]';
3 | }
4 |
--------------------------------------------------------------------------------
/src/utils/keepAliveDecorator.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import hoistNonReactStatics from 'hoist-non-react-statics';
3 | import IdentificationContext from '../contexts/IdentificationContext';
4 | import Consumer from '../components/Consumer';
5 | import {LIFECYCLE} from '../components/Provider';
6 | import md5 from './md5';
7 | import {warn} from './debug';
8 | import getKeyByFiberNode from './getKeyByFiberNode';
9 | import withIdentificationContextConsumer, {IIdentificationContextConsumerComponentProps} from './withIdentificationContextConsumer';
10 | import withKeepAliveContextConsumer, {IKeepAliveContextConsumerComponentProps} from './withKeepAliveContextConsumer';
11 | import shallowEqual from './shallowEqual';
12 | import getKeepAlive from './getKeepAlive';
13 |
14 | export enum COMMAND {
15 | UNACTIVATE = 'unactivate',
16 | UNMOUNT = 'unmount',
17 | ACTIVATE = 'activate',
18 | CURRENT_UNMOUNT = 'current_unmount',
19 | CURRENT_UNACTIVATE = 'current_unactivate',
20 | }
21 |
22 | interface IListenUpperKeepAliveContainerProps extends IIdentificationContextConsumerComponentProps, IKeepAliveContextConsumerComponentProps {
23 | disabled?: boolean;
24 | name?: string;
25 | }
26 |
27 | interface IListenUpperKeepAliveContainerState {
28 | activated: boolean;
29 | }
30 |
31 | interface ITriggerLifecycleContainerProps extends IKeepAliveContextConsumerComponentProps {
32 | propKey: string;
33 | extra?: any;
34 | keepAlive: boolean;
35 | getCombinedKeepAlive: () => boolean;
36 | }
37 |
38 | /**
39 | * Decorating the component, the main function is to listen to events emitted by the upper component, triggering events of the current component.
40 | *
41 | * @export
42 | * @template P
43 | * @param {React.ComponentType} Component
44 | * @returns {React.ComponentType}
45 | */
46 | export default function keepAliveDecorator
(Component: React.ComponentType): React.ComponentType {
47 | class TriggerLifecycleContainer extends React.PureComponent {
48 | private identification: string;
49 |
50 | private activated = false;
51 |
52 | private ifStillActivate = false;
53 |
54 | // Let the lifecycle of the cached component be called normally.
55 | private needActivate = true;
56 |
57 | private lifecycle = LIFECYCLE.MOUNTED;
58 |
59 | constructor(props: ITriggerLifecycleContainerProps, ...args: any) {
60 | super(props, ...args);
61 | const {
62 | _keepAliveContextProps: {
63 | cache,
64 | },
65 | } = props;
66 | if (!cache) {
67 | warn('[React Keep Alive] You should not use outside a .');
68 | }
69 | }
70 |
71 | public componentDidMount() {
72 | if (!this.ifStillActivate) {
73 | this.activate();
74 | }
75 | const {
76 | keepAlive,
77 | _keepAliveContextProps: {
78 | eventEmitter,
79 | },
80 | } = this.props;
81 | if (keepAlive) {
82 | this.needActivate = true;
83 | eventEmitter.emit([this.identification, COMMAND.ACTIVATE]);
84 | }
85 | }
86 |
87 | public componentDidCatch() {
88 | if (!this.activated) {
89 | this.activate();
90 | }
91 | }
92 |
93 | public componentWillUnmount() {
94 | const {
95 | getCombinedKeepAlive,
96 | _keepAliveContextProps: {
97 | eventEmitter,
98 | isExisted,
99 | },
100 | } = this.props;
101 | const keepAlive = getCombinedKeepAlive();
102 | if (!keepAlive || !isExisted()) {
103 | eventEmitter.emit([this.identification, COMMAND.CURRENT_UNMOUNT]);
104 | eventEmitter.emit([this.identification, COMMAND.UNMOUNT]);
105 | }
106 | // When the Provider components are unmounted, the cache is not needed,
107 | // so you don't have to execute the componentWillUnactivate lifecycle.
108 | if (keepAlive && isExisted()) {
109 | eventEmitter.emit([this.identification, COMMAND.CURRENT_UNACTIVATE]);
110 | eventEmitter.emit([this.identification, COMMAND.UNACTIVATE]);
111 | }
112 | }
113 |
114 | private activate = () => {
115 | this.activated = true;
116 | }
117 |
118 | private reactivate = () => {
119 | this.ifStillActivate = false;
120 | this.forceUpdate();
121 | }
122 |
123 | private isNeedActivate = () => {
124 | return this.needActivate;
125 | }
126 |
127 | private notNeedActivate = () => {
128 | this.needActivate = false;
129 | }
130 |
131 | private getLifecycle = () => {
132 | return this.lifecycle;
133 | }
134 |
135 | private setLifecycle = (lifecycle: LIFECYCLE) => {
136 | this.lifecycle = lifecycle;
137 | }
138 |
139 | public render() {
140 | const {
141 | propKey,
142 | keepAlive,
143 | extra,
144 | getCombinedKeepAlive,
145 | _keepAliveContextProps: {
146 | isExisted,
147 | storeElement,
148 | cache,
149 | eventEmitter,
150 | setCache,
151 | unactivate,
152 | providerIdentification,
153 | },
154 | ...wrapperProps
155 | } = this.props;
156 | if (!this.identification) {
157 | // We need to generate a corresponding unique identifier based on the information of the component.
158 | this.identification = md5(
159 | `${providerIdentification}${propKey}`,
160 | );
161 | // The last activated component must be unactivated before it can be activated again.
162 | const currentCache = cache[this.identification];
163 | if (currentCache) {
164 | this.ifStillActivate = currentCache.activated as boolean;
165 | currentCache.ifStillActivate = this.ifStillActivate;
166 | currentCache.reactivate = this.reactivate;
167 | }
168 | }
169 | const {
170 | isNeedActivate,
171 | notNeedActivate,
172 | activated,
173 | getLifecycle,
174 | setLifecycle,
175 | identification,
176 | ifStillActivate,
177 | } = this;
178 | return !ifStillActivate
179 | ? (
180 |
187 |
198 |
211 |
212 |
213 | )
214 | : null;
215 | }
216 | }
217 |
218 | class ListenUpperKeepAliveContainer extends React.Component {
219 | private combinedKeepAlive: boolean;
220 |
221 | public state = {
222 | activated: true,
223 | };
224 |
225 | private activate: () => void;
226 |
227 | private unactivate: () => void;
228 |
229 | private unmount: () => void;
230 |
231 | public shouldComponentUpdate(nextProps: IListenUpperKeepAliveContainerProps, nextState: IListenUpperKeepAliveContainerState) {
232 | if (this.state.activated !== nextState.activated) {
233 | return true;
234 | }
235 | const {
236 | _keepAliveContextProps,
237 | _identificationContextProps,
238 | ...rest
239 | } = this.props;
240 | const {
241 | _keepAliveContextProps: nextKeepAliveContextProps,
242 | _identificationContextProps: nextIdentificationContextProps,
243 | ...nextRest
244 | } = nextProps;
245 | if (!shallowEqual(rest, nextRest)) {
246 | return true;
247 | }
248 | if (
249 | !shallowEqual(_keepAliveContextProps, nextKeepAliveContextProps) ||
250 | !shallowEqual(_identificationContextProps, nextIdentificationContextProps)
251 | ) {
252 | return true;
253 | }
254 | return false;
255 | }
256 |
257 | public componentDidMount() {
258 | this.listenUpperKeepAlive();
259 | }
260 |
261 | public componentWillUnmount() {
262 | this.unlistenUpperKeepAlive();
263 | }
264 |
265 | private listenUpperKeepAlive() {
266 | const {identification, eventEmitter} = this.props._identificationContextProps;
267 | if (!identification) {
268 | return;
269 | }
270 | eventEmitter.on(
271 | [identification, COMMAND.ACTIVATE],
272 | this.activate = () => this.setState({activated: true}),
273 | true,
274 | );
275 | eventEmitter.on(
276 | [identification, COMMAND.UNACTIVATE],
277 | this.unactivate = () => this.setState({activated: false}),
278 | true,
279 | );
280 | eventEmitter.on(
281 | [identification, COMMAND.UNMOUNT],
282 | this.unmount = () => this.setState({activated: false}),
283 | true,
284 | );
285 | }
286 |
287 | private unlistenUpperKeepAlive() {
288 | const {identification, eventEmitter} = this.props._identificationContextProps;
289 | if (!identification) {
290 | return;
291 | }
292 | eventEmitter.off([identification, COMMAND.ACTIVATE], this.activate);
293 | eventEmitter.off([identification, COMMAND.UNACTIVATE], this.unactivate);
294 | eventEmitter.off([identification, COMMAND.UNMOUNT], this.unmount);
295 | }
296 |
297 | private getCombinedKeepAlive = () => {
298 | return this.combinedKeepAlive;
299 | }
300 |
301 | public render() {
302 | const {
303 | _identificationContextProps: {
304 | identification,
305 | keepAlive: upperKeepAlive,
306 | getLifecycle,
307 | },
308 | disabled,
309 | name,
310 | ...wrapperProps
311 | } = this.props;
312 | const {activated} = this.state;
313 | const {
314 | _keepAliveContextProps: {
315 | include,
316 | exclude,
317 | },
318 | } = wrapperProps;
319 | // When the parent KeepAlive component is mounted or unmounted,
320 | // use the keepAlive prop of the parent KeepAlive component.
321 | const propKey = name || getKeyByFiberNode((this as any)._reactInternalFiber);
322 | if (!propKey) {
323 | warn('[React Keep Alive] components must have key or name.');
324 | return null;
325 | }
326 | const newKeepAlive = getKeepAlive(propKey, include, exclude, disabled);
327 | this.combinedKeepAlive = getLifecycle === undefined || getLifecycle() === LIFECYCLE.UPDATING
328 | ? newKeepAlive
329 | : identification
330 | ? upperKeepAlive && newKeepAlive
331 | : newKeepAlive;
332 | return activated
333 | ? (
334 |
341 | )
342 | : null;
343 | }
344 | }
345 |
346 | const KeepAlive = withKeepAliveContextConsumer(
347 | withIdentificationContextConsumer(ListenUpperKeepAliveContainer)
348 | ) as any;
349 |
350 | return hoistNonReactStatics(KeepAlive, Component);
351 | }
352 |
--------------------------------------------------------------------------------
/src/utils/md5.ts:
--------------------------------------------------------------------------------
1 | import md5 from 'js-md5';
2 | import {prefix} from './createUniqueIdentification';
3 |
4 | export default function createMD5(value: string = '', length = 6) {
5 | return `${prefix}-${md5(value).substr(0, length)}`;
6 | }
7 |
--------------------------------------------------------------------------------
/src/utils/noop.ts:
--------------------------------------------------------------------------------
1 | const noop = () => undefined;
2 |
3 | export default noop;
4 |
--------------------------------------------------------------------------------
/src/utils/shallowEqual.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * From react
3 | */
4 | function is(x: any, y: any) {
5 | return (
6 | (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
7 | );
8 | }
9 |
10 | const hasOwnProperty = Object.prototype.hasOwnProperty;
11 |
12 | function shallowEqual(objA: object, objB: object) {
13 | if (is(objA, objB)) {
14 | return true;
15 | }
16 |
17 | if (
18 | typeof objA !== 'object' ||
19 | objA === null ||
20 | typeof objB !== 'object' ||
21 | objB === null
22 | ) {
23 | return false;
24 | }
25 |
26 | const keysA = Object.keys(objA);
27 | const keysB = Object.keys(objB);
28 |
29 | if (keysA.length !== keysB.length) {
30 | return false;
31 | }
32 |
33 | // Test for A's keys different from B.
34 | for (const key of keysA) {
35 | if (
36 | !hasOwnProperty.call(objB, key) ||
37 | !is(objA[key], objB[key])
38 | ) {
39 | return false;
40 | }
41 | }
42 |
43 | return true;
44 | }
45 |
46 | export default shallowEqual;
47 |
--------------------------------------------------------------------------------
/src/utils/useKeepAliveEffect.tsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect, useContext, useRef} from 'react';
2 | import {warn} from './debug';
3 | import {COMMAND} from './keepAliveDecorator';
4 | import IdentificationContext, {IIdentificationContextProps} from '../contexts/IdentificationContext';
5 |
6 | export default function useKeepAliveEffect(effect: React.EffectCallback) {
7 | if (!useEffect) {
8 | warn('[React Keep Alive] useKeepAliveEffect API requires react 16.8 or later.');
9 | }
10 | const {
11 | eventEmitter,
12 | identification,
13 | } = useContext(IdentificationContext);
14 | const effectRef: React.MutableRefObject = useRef(effect);
15 | effectRef.current = effect;
16 | useEffect(() => {
17 | let bindActivate: (() => void) | null = null;
18 | let bindUnactivate: (() => void) | null = null;
19 | let bindUnmount: (() => void) | null = null;
20 | let effectResult = effectRef.current();
21 | let unmounted = false;
22 | eventEmitter.on(
23 | [identification, COMMAND.ACTIVATE],
24 | bindActivate = () => {
25 | // Delayed update
26 | Promise.resolve().then(() => {
27 | effectResult = effectRef.current();
28 | });
29 | unmounted = false;
30 | },
31 | true,
32 | );
33 | eventEmitter.on(
34 | [identification, COMMAND.UNACTIVATE],
35 | bindUnactivate = () => {
36 | if (effectResult) {
37 | effectResult();
38 | unmounted = true;
39 | }
40 | },
41 | true,
42 | );
43 | eventEmitter.on(
44 | [identification, COMMAND.UNMOUNT],
45 | bindUnmount = () => {
46 | if (effectResult) {
47 | effectResult();
48 | unmounted = true;
49 | }
50 | },
51 | true,
52 | );
53 | return () => {
54 | if (effectResult && !unmounted) {
55 | effectResult();
56 | }
57 | eventEmitter.off(
58 | [identification, COMMAND.ACTIVATE],
59 | bindActivate,
60 | );
61 | eventEmitter.off(
62 | [identification, COMMAND.UNACTIVATE],
63 | bindUnactivate,
64 | );
65 | eventEmitter.off(
66 | [identification, COMMAND.UNMOUNT],
67 | bindUnmount,
68 | );
69 | };
70 | }, []);
71 | }
72 |
--------------------------------------------------------------------------------
/src/utils/withIdentificationContextConsumer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import IdentificationContext, {IIdentificationContextProps} from '../contexts/IdentificationContext';
3 | import getDisplayName from './getDisplayName';
4 |
5 | export interface IIdentificationContextConsumerComponentProps {
6 | _identificationContextProps: IIdentificationContextProps;
7 | }
8 |
9 | export const withIdentificationContextConsumerDisplayName = 'withIdentificationContextConsumer';
10 |
11 | export default function withIdentificationContextConsumer(Component: React.ComponentType) {
12 | const WithIdentificationContextConsumer = (props: P) => (
13 |
14 | {(contextProps: IIdentificationContextProps) => }
15 |
16 | );
17 |
18 | WithIdentificationContextConsumer.displayName = `${withIdentificationContextConsumerDisplayName}(${getDisplayName(Component)})`;
19 | return WithIdentificationContextConsumer;
20 | }
21 |
--------------------------------------------------------------------------------
/src/utils/withKeepAliveContextConsumer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import KeepAliveContext, {IKeepAliveContextProps} from '../contexts/KeepAliveContext';
3 | import getDisplayName from './getDisplayName';
4 |
5 | export interface IKeepAliveContextConsumerComponentProps {
6 | _keepAliveContextProps: IKeepAliveContextProps;
7 | }
8 |
9 | export const WithKeepAliveContextConsumerDisplayName = 'withKeepAliveContextConsumer';
10 |
11 | export default function withKeepAliveContextConsumer(Component: React.ComponentType) {
12 | const WithKeepAliveContextConsumer = (props: P) => (
13 |
14 | {(contextProps: IKeepAliveContextProps) => }
15 |
16 | );
17 |
18 | WithKeepAliveContextConsumer.displayName = `${WithKeepAliveContextConsumerDisplayName}(${getDisplayName(Component)})`;
19 | return WithKeepAliveContextConsumer;
20 | }
21 |
--------------------------------------------------------------------------------
/test/Comment.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {mount} from 'enzyme';
3 | import Comment from '../src/components/Comment';
4 |
5 | describe('', () => {
6 | it('the render function will render a div element', () => {
7 | const wrapper = mount(test);
8 | expect(wrapper.html()).toEqual('');
9 | });
10 |
11 |
12 | it('rendered will be replaced with comment nodes', () => {
13 | const wrapper = mount(
14 |
15 | test
16 |
17 | );
18 | expect(wrapper.html()).toContain('');
19 | });
20 |
21 | it('the comment node will be restored to
when uninstalling', () => {
22 | const componentWillUnmount = Comment.prototype.componentWillUnmount;
23 | const wrapper = mount(
24 |
25 | test
26 |
27 | );
28 | Comment.prototype.componentWillUnmount = function () {
29 | componentWillUnmount.call(this);
30 | expect(wrapper.html()).toContain('
');
31 | }
32 | wrapper.unmount();
33 | Comment.prototype.componentWillUnmount = componentWillUnmount;
34 | });
35 |
36 | it('children of
will become empty strings if they are not of type string', () => {
37 | const wrapper = mount(
38 |
41 | );
42 | expect(wrapper.html()).toContain('');
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/test/KeepAlive.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {mount} from 'enzyme';
3 | import {KeepAlive} from '../src';
4 |
5 | class Test extends React.Component {
6 | state = {
7 | index: 0,
8 | };
9 |
10 | handleAdd = () => {
11 | this.setState(({index}) => ({
12 | index: index + 1,
13 | }));
14 | }
15 |
16 | render() {
17 | return this.state.index;
18 | }
19 | }
20 |
21 | describe('', () => {
22 | it(' not will report an error', () => {
23 | expect(() => {
24 | mount(
25 |
26 |
27 | ,
28 | );
29 | }).toThrow('[React Keep Alive]');
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/test/setup.js:
--------------------------------------------------------------------------------
1 | import {configure} from 'enzyme';
2 | import Adapter from 'enzyme-adapter-react-16';
3 |
4 | console.error = jest.fn(error => {
5 | throw new Error(error);
6 | });
7 |
8 | configure({
9 | adapter: new Adapter(),
10 | });
11 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "outDir": "es",
5 | "module": "esnext",
6 | "esModuleInterop": true,
7 | "target": "es5",
8 | "lib": ["es6", "dom"],
9 | "declaration": true,
10 | "sourceMap": false,
11 | "jsx": "react",
12 | "moduleResolution": "node",
13 | "rootDir": "src",
14 | "forceConsistentCasingInFileNames": true,
15 | "noImplicitThis": true,
16 | "noImplicitAny": true,
17 | "strictNullChecks": true,
18 | "suppressImplicitAnyIndexErrors": true,
19 | "experimentalDecorators": true,
20 | "noUnusedLocals": true,
21 | "typeRoots": [
22 | "node_modules/@types",
23 | "typings",
24 | ],
25 | },
26 | "exclude": [
27 | "node_modules",
28 | "jest",
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "error",
3 | "extends": [
4 | "tslint:recommended"
5 | ],
6 | "rules": {
7 | "max-line-length": false,
8 | "no-console": false,
9 | "no-debugger": false,
10 | "quotemark": [true, "single", "jsx-double"],
11 | "trailing-comma": [true, {"multiline": "ignore", "singleline": "never"}],
12 | "ordered-imports": false,
13 | "member-ordering": false,
14 | "max-classes-per-file": false,
15 | "no-unsafe-finally": false,
16 | "object-literal-sort-keys": false,
17 | "space-before-function-paren": false,
18 | "no-shadowed-variable": false,
19 | "arrow-parens": [true, "ban-single-arg-parens"]
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackPlugin = require('html-webpack-plugin');
2 | const path = require('path');
3 | const webpack = require('webpack');
4 |
5 | const ROOT = path.join(__dirname, 'demo');
6 | const SRC = path.join(ROOT, 'src');
7 |
8 | module.exports = {
9 | mode: 'development',
10 | devtool: 'source-map',
11 | entry: {
12 | index: path.join(SRC, 'index.js'),
13 | },
14 | output: {
15 | path: path.join(ROOT, 'build'),
16 | filename: 'static/[name].js',
17 | publicPath: '/',
18 | },
19 | plugins: [
20 | new HtmlWebpackPlugin({
21 | filename: 'index.html',
22 | inject: true,
23 | template: path.join(ROOT, 'index.html'),
24 | }),
25 | ],
26 | module: {
27 | rules: [
28 | {
29 | test: /\.js$/,
30 | use: ['babel-loader'],
31 | include: SRC,
32 | },
33 | {
34 | test: /\.css$/,
35 | use: ['style-loader', 'css-loader'],
36 | },
37 | ],
38 | },
39 | };
40 |
--------------------------------------------------------------------------------