├── .babelrc ├── .gitignore ├── CHANGELOG.md ├── README.md ├── dist ├── index.js └── lib.js ├── examples └── cache_page_with_scroll_restoration │ └── index.js ├── package.json ├── src ├── index.js └── lib.js └── test ├── .babelrc ├── dist ├── bundle.js └── bundle.js.map ├── index.html ├── package.json ├── src └── entry.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-0"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 1.1.1 2 | 3 | - introduce `prop-types` package 4 | 5 | ### 1.1.0 6 | 7 | - support add/remove hooks 8 | - fix bugs of switch hooks 9 | 10 | ### 1.0.0 11 | 12 | First Version -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React View Cache 2 | cache react views. 3 | 4 | ## Introduction 5 | With react-view-cache, you can render your components within a special **Cache** component. When you render another 6 | component within the same cache, the previous component will not unmount immediately, but just hide by being set as 7 | `display: none`, with all state reserved. 8 | 9 | It's great to cache pages in a SPA. By saving and restoring scroll in cache hooks, you will get an optimized app. When 10 | you browse back and forward, the previous page shows up immediately with the old scroll without any reloading. More, you 11 | will also make less effort to manage data and states between pages. 12 | 13 | ## Basic Usage 14 | ``` 15 | import createCache from "react-view-cache"; 16 | const Cache = createCache(); 17 | 18 | ... 19 | 20 | const view = ; // your component 21 | const viewId = ...; // unique cache id for this component 22 | 23 | ... 24 | 25 | 26 | ``` 27 | 28 | ## Options 29 | You can pass in options as the args of `createCache`, or modify them on the created `Cache` instance. 30 | ``` 31 | const Cache = createCache(options) 32 | // or 33 | Cache.options.xxx = ... 34 | ``` 35 | 36 | * cacheTime - number, milliseconds, default 5 * 60 * 1000 37 | * cacheLimit - number, default 5 38 | * hooks 39 | * beforeSwitch(oldViewId, newViewId) 40 | * afterSwitch(oldViewId, newViewId) 41 | * beforeAdd(viewId) 42 | * afterAdd(viewId) 43 | * beforeRemove(viewId) 44 | * afterRemove(viewId) 45 | 46 | ## Cache 47 | Cache will render component by current props, and the previous components will be hidden. 48 | 49 | ### props 50 | * viewId - unique cache id for this component 51 | * view - one of the following types 52 | * react node 53 | * func({isActive}): react node 54 | * [cacheTime] - optional, cache time for this component 55 | 56 | ## Examples 57 | - [Cache page with scroll restoration](https://github.com/zhaoyao91/react-view-cache/blob/master/examples/cache_page_with_scroll_restoration/index.js) 58 | 59 | ## License 60 | MIT -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | exports.default = createCache; 10 | 11 | var _react = require("react"); 12 | 13 | var _react2 = _interopRequireDefault(_react); 14 | 15 | var _lib = require("./lib"); 16 | 17 | var _propTypes = require("prop-types"); 18 | 19 | var _propTypes2 = _interopRequireDefault(_propTypes); 20 | 21 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 22 | 23 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 24 | 25 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 26 | 27 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 28 | 29 | var DefaultOptions = { 30 | cacheTime: 5 * 60 * 1000, // 5 minutes 31 | cacheLimit: 5, 32 | hooks: { 33 | beforeSwitch: function beforeSwitch(oldViewId, newViewId) {}, 34 | afterSwitch: function afterSwitch(oldViewId, newViewId) {}, 35 | beforeAdd: function beforeAdd(viewId) {}, 36 | afterAdd: function afterAdd(viewId) {}, 37 | beforeRemove: function beforeRemove(viewId) {}, 38 | afterRemove: function afterRemove(viewId) {} 39 | } 40 | }; 41 | 42 | function createCache(options) { 43 | var Cache = function (_React$Component) { 44 | _inherits(Cache, _React$Component); 45 | 46 | function Cache() { 47 | var _ref; 48 | 49 | var _temp, _this, _ret; 50 | 51 | _classCallCheck(this, Cache); 52 | 53 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 54 | args[_key] = arguments[_key]; 55 | } 56 | 57 | return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref = Cache.__proto__ || Object.getPrototypeOf(Cache)).call.apply(_ref, [this].concat(args))), _this), _this.state = { 58 | activeId: '', 59 | 60 | // {id, view, cacheTime, cacheTimer, lastActivatedAt} 61 | viewItems: [] 62 | }, _temp), _possibleConstructorReturn(_this, _ret); 63 | } 64 | 65 | _createClass(Cache, [{ 66 | key: "componentDidMount", 67 | value: function componentDidMount() { 68 | this.refresh(this.props); 69 | } 70 | }, { 71 | key: "componentWillReceiveProps", 72 | value: function componentWillReceiveProps(nextProps) { 73 | this.refresh(nextProps); 74 | } 75 | }, { 76 | key: "componentWillUnmount", 77 | value: function componentWillUnmount() { 78 | var viewItems = this.state.viewItems; 79 | 80 | viewItems.forEach(function (item) { 81 | return clearTimeout(item.cacheTimer); 82 | }); 83 | 84 | Cache.options.hooks.beforeSwitch(this.state.activeId, ''); 85 | Cache.options.hooks.afterSwitch(this.state.activeId, ''); 86 | 87 | (0, _lib.getIds)(viewItems).forEach(function (id) { 88 | Cache.options.hooks.beforeRemove(id); 89 | Cache.options.hooks.afterRemove(id); 90 | }); 91 | } 92 | }, { 93 | key: "componentWillUpdate", 94 | value: function componentWillUpdate(nextProps, nextState) { 95 | var oldId = this.state.activeId; 96 | var newId = nextState.activeId; 97 | var oldIds = (0, _lib.getIds)(this.state.viewItems); 98 | var newIds = (0, _lib.getIds)(nextState.viewItems); 99 | var addedIds = (0, _lib.difference)(newIds, oldIds); 100 | var removedIds = (0, _lib.difference)(oldIds, newIds); 101 | 102 | if (oldId !== newId) Cache.options.hooks.beforeSwitch(oldId, newId); 103 | addedIds.forEach(function (id) { 104 | return Cache.options.hooks.beforeAdd(id); 105 | }); 106 | removedIds.forEach(function (id) { 107 | return Cache.options.hooks.beforeRemove(id); 108 | }); 109 | } 110 | }, { 111 | key: "componentDidUpdate", 112 | value: function componentDidUpdate(prevProps, prevState) { 113 | var oldId = prevState.activeId; 114 | var newId = this.state.activeId; 115 | var oldIds = (0, _lib.getIds)(prevState.viewItems); 116 | var newIds = (0, _lib.getIds)(this.state.viewItems); 117 | var addedIds = (0, _lib.difference)(newIds, oldIds); 118 | var removedIds = (0, _lib.difference)(oldIds, newIds); 119 | 120 | if (oldId !== newId) Cache.options.hooks.afterSwitch(oldId, newId); 121 | addedIds.forEach(function (id) { 122 | return Cache.options.hooks.afterAdd(id); 123 | }); 124 | removedIds.forEach(function (id) { 125 | return Cache.options.hooks.afterRemove(id); 126 | }); 127 | } 128 | }, { 129 | key: "render", 130 | value: function render() { 131 | var _state = this.state, 132 | activeId = _state.activeId, 133 | viewItems = _state.viewItems; 134 | 135 | 136 | return _react2.default.createElement( 137 | "div", 138 | null, 139 | viewItems.map(function (item) { 140 | var isActive = item.id === activeId; 141 | return _react2.default.createElement( 142 | "div", 143 | { key: item.id, style: { display: isActive ? undefined : 'none' } }, 144 | typeof item.view === 'function' ? _react2.default.createElement(View, { args: { isActive: isActive }, view: item.view }) : item.view 145 | ); 146 | }) 147 | ); 148 | } 149 | }, { 150 | key: "refresh", 151 | value: function refresh(props) { 152 | var viewId = props.viewId, 153 | view = props.view, 154 | cacheTime = props.cacheTime; 155 | var _state2 = this.state, 156 | activeId = _state2.activeId, 157 | viewItems = _state2.viewItems; 158 | 159 | 160 | var newViewItems = viewItems.slice(); 161 | 162 | if (!viewId) { 163 | // set cache timer for switch-out item 164 | var preViewItem = newViewItems.find(function (item) { 165 | return item.id === activeId; 166 | }); 167 | this.setCacheTimer(preViewItem); 168 | } else { 169 | var viewItem = newViewItems.find(function (item) { 170 | return item.id === viewId; 171 | }); 172 | if (viewItem) { 173 | // update item 174 | viewItem.view = view; 175 | viewItem.cacheTime = cacheTime; 176 | viewItem.lastActivatedAt = new Date(); 177 | 178 | if (viewId !== activeId) { 179 | // remove cache timer for switch-in item 180 | clearTimeout(viewItem.cacheTimer); 181 | 182 | // set cache timer for switch-out item 183 | var _preViewItem = newViewItems.find(function (item) { 184 | return item.id === activeId; 185 | }); 186 | this.setCacheTimer(_preViewItem); 187 | } 188 | } else { 189 | // create new item 190 | var newViewItem = { 191 | id: viewId, 192 | view: view, 193 | cacheTime: cacheTime, 194 | lastActivatedAt: new Date() 195 | }; 196 | newViewItems.push(newViewItem); 197 | 198 | // set cache timer for previous item 199 | var _preViewItem2 = newViewItems.find(function (item) { 200 | return item.id === activeId; 201 | }); 202 | this.setCacheTimer(_preViewItem2); 203 | 204 | // remove oldest surplus item 205 | if (newViewItems.length > this.getCacheLimit()) { 206 | var oldestItem = newViewItems.reduce(function (min, cur) { 207 | return cur.lastActivatedAt < min.lastActivatedAt ? cur : min; 208 | }, newViewItems[0]); 209 | if (oldestItem) { 210 | clearTimeout(oldestItem.cacheTimer); 211 | newViewItems.splice(newViewItems.findIndex(function (item) { 212 | return item === oldestItem; 213 | }), 1); 214 | } 215 | } 216 | } 217 | } 218 | 219 | // update state 220 | this.setState({ 221 | activeId: viewId, 222 | viewItems: newViewItems 223 | }); 224 | } 225 | }, { 226 | key: "expireItem", 227 | value: function expireItem(id) { 228 | var viewItems = this.state.viewItems; 229 | 230 | var viewItem = viewItems.find(function (item) { 231 | return item.id === id; 232 | }); 233 | if (viewItem) { 234 | clearTimeout(viewItem.cacheTimer); 235 | var newViewItems = viewItems.filter(function (item) { 236 | return item !== viewItem; 237 | }); 238 | this.setState({ viewItems: newViewItems }); 239 | } 240 | } 241 | }, { 242 | key: "setCacheTimer", 243 | value: function setCacheTimer(item) { 244 | var _this2 = this; 245 | 246 | if (item) { 247 | clearTimeout(item.cacheTimer); 248 | var cacheTime = this.getCacheTime(item); 249 | item.cacheTimer = setTimeout(function () { 250 | return _this2.expireItem(item.id); 251 | }, cacheTime); 252 | } 253 | } 254 | }, { 255 | key: "getCacheTime", 256 | value: function getCacheTime(item) { 257 | if (item.cacheTime !== undefined) return item.cacheTime;else return Cache.options.cacheTime; 258 | } 259 | }, { 260 | key: "getCacheLimit", 261 | value: function getCacheLimit() { 262 | return Cache.options.cacheLimit; 263 | } 264 | }]); 265 | 266 | return Cache; 267 | }(_react2.default.Component); 268 | 269 | // optimize re-render 270 | 271 | 272 | Cache.options = (0, _lib.deepAssign)((0, _lib.deepAssign)({}, DefaultOptions), options); 273 | Cache.propTypes = { 274 | viewId: _propTypes2.default.string.isRequired, 275 | view: _propTypes2.default.oneOfType([_propTypes2.default.func, _propTypes2.default.node]).isRequired, 276 | cacheTime: _propTypes2.default.number 277 | }; 278 | 279 | var View = function (_React$Component2) { 280 | _inherits(View, _React$Component2); 281 | 282 | function View() { 283 | _classCallCheck(this, View); 284 | 285 | return _possibleConstructorReturn(this, (View.__proto__ || Object.getPrototypeOf(View)).apply(this, arguments)); 286 | } 287 | 288 | _createClass(View, [{ 289 | key: "shouldComponentUpdate", 290 | value: function shouldComponentUpdate(nextProps) { 291 | return !(0, _lib.deepEqual)(this.props, nextProps); 292 | } 293 | }, { 294 | key: "render", 295 | value: function render() { 296 | var _props = this.props, 297 | args = _props.args, 298 | view = _props.view; 299 | 300 | return view(args); 301 | } 302 | }]); 303 | 304 | return View; 305 | }(_react2.default.Component); 306 | 307 | View.propTypes = { 308 | args: _propTypes2.default.object, 309 | view: _propTypes2.default.func.isRequired 310 | }; 311 | 312 | 313 | return Cache; 314 | } -------------------------------------------------------------------------------- /dist/lib.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; 8 | 9 | exports.deepEqual = deepEqual; 10 | exports.deepAssign = deepAssign; 11 | exports.getIds = getIds; 12 | exports.difference = difference; 13 | function deepEqual(a, b) { 14 | if (!isObject(a) || !isObject(b)) return a === b;else return deepEqualInner(a, b) && deepEqualInner(b, a); 15 | } 16 | 17 | function deepEqualInner(a, b) { 18 | for (var key in a) { 19 | if (!deepEqual(a[key], b[key])) return false; 20 | } 21 | return true; 22 | } 23 | 24 | function isObject(x) { 25 | return (typeof x === 'undefined' ? 'undefined' : _typeof(x)) === 'object' && x !== null; 26 | } 27 | 28 | function deepAssign(base, extension) { 29 | if (!isObject(base)) base = {}; 30 | if (!isObject(extension)) extension = {}; 31 | 32 | for (var key in extension) { 33 | if (!isObject(extension[key])) base[key] = extension[key];else base[key] = deepAssign(base[key], extension[key]); 34 | } 35 | 36 | return base; 37 | } 38 | 39 | function getIds(items) { 40 | return items.map(function (item) { 41 | return item.id; 42 | }); 43 | } 44 | 45 | function difference(a, b) { 46 | var result = []; 47 | var _iteratorNormalCompletion = true; 48 | var _didIteratorError = false; 49 | var _iteratorError = undefined; 50 | 51 | try { 52 | for (var _iterator = a[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { 53 | var value = _step.value; 54 | 55 | if (b.indexOf(value) < 0) result.push(value); 56 | } 57 | } catch (err) { 58 | _didIteratorError = true; 59 | _iteratorError = err; 60 | } finally { 61 | try { 62 | if (!_iteratorNormalCompletion && _iterator.return) { 63 | _iterator.return(); 64 | } 65 | } finally { 66 | if (_didIteratorError) { 67 | throw _iteratorError; 68 | } 69 | } 70 | } 71 | 72 | return result; 73 | } -------------------------------------------------------------------------------- /examples/cache_page_with_scroll_restoration/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import createCache from "react-view-cache"; 3 | import {FlowRouter} from "meteor/kadira:flow-router"; 4 | import {mount} from "react-mounter"; 5 | 6 | import SomePage from "./some_page"; 7 | 8 | // 1. setup things for scroll restoration 9 | const scrollStore = {}; 10 | const scrollHelper = { 11 | saveScroll(id) { 12 | scrollStore[ id ] = document.body.scrollTop; 13 | }, 14 | 15 | loadScroll(id) { 16 | document.body.scrollTop = scrollStore[ id ]; 17 | }, 18 | 19 | clearScroll(id) { 20 | delete scrollStore[ id ]; 21 | } 22 | }; 23 | 24 | // 2. create cache 25 | // - config it with options 26 | // - config hooks for scroll restoration 27 | const Cache = createCache({ 28 | cacheTime: 5 * 60 * 1000, // optional 29 | cacheLimit: 10, // optional 30 | hooks: { 31 | beforeSwitch(oldId, newId) { 32 | if (oldId) scrollHelper.saveScroll(oldId); 33 | }, 34 | afterSwitch(oldId, newId) { 35 | if (newId) scrollHelper.loadScroll(newId); 36 | }, 37 | beforeRemove(id) { 38 | if (id) scrollHelper.clearScroll(id); 39 | } 40 | } 41 | }); 42 | 43 | // 3. mount pages ... 44 | FlowRouter.route('/page_path', { 45 | name: 'page_name', 46 | action(params, query) { 47 | mount(Cache, { 48 | viewId: 'pageId', 49 | view: , 50 | }) 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-view-cache", 3 | "version": "1.1.1", 4 | "description": "cache react views", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/zhaoyao91/react-view-cache.git" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/zhaoyao91/react-view-cache/issues" 11 | }, 12 | "homepage": "https://github.com/zhaoyao91/react-view-cache#readme", 13 | "keywords": [ 14 | "react", 15 | "view", 16 | "cache" 17 | ], 18 | "main": "dist/index.js", 19 | "scripts": { 20 | "build": "babel src -d dist" 21 | }, 22 | "author": "zhaoyao91", 23 | "license": "MIT", 24 | "peerDependencies": { 25 | "react": "^0.14.0 || ^15.0.0" 26 | }, 27 | "devDependencies": { 28 | "babel-cli": "^6.16.0", 29 | "babel-preset-es2015": "^6.16.0", 30 | "babel-preset-react": "^6.16.0", 31 | "babel-preset-stage-0": "^6.16.0" 32 | }, 33 | "dependencies": { 34 | "prop-types": "^15.0.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {deepEqual, deepAssign, getIds, difference} from "./lib"; 3 | import PropTypes from 'prop-types'; 4 | 5 | const DefaultOptions = { 6 | cacheTime: 5 * 60 * 1000, // 5 minutes 7 | cacheLimit: 5, 8 | hooks: { 9 | beforeSwitch(oldViewId, newViewId){ 10 | }, 11 | afterSwitch(oldViewId, newViewId){ 12 | }, 13 | beforeAdd(viewId){ 14 | }, 15 | afterAdd(viewId){ 16 | }, 17 | beforeRemove(viewId){ 18 | }, 19 | afterRemove(viewId){ 20 | }, 21 | } 22 | }; 23 | 24 | export default function createCache(options) { 25 | class Cache extends React.Component { 26 | static options = deepAssign(deepAssign({}, DefaultOptions), options); 27 | 28 | static propTypes = { 29 | viewId: PropTypes.string.isRequired, 30 | view: PropTypes.oneOfType([ 31 | PropTypes.func, 32 | PropTypes.node, 33 | ]).isRequired, 34 | cacheTime: PropTypes.number 35 | }; 36 | 37 | state = { 38 | activeId: '', 39 | 40 | // {id, view, cacheTime, cacheTimer, lastActivatedAt} 41 | viewItems: [], 42 | }; 43 | 44 | componentDidMount() { 45 | this.refresh(this.props); 46 | } 47 | 48 | componentWillReceiveProps(nextProps) { 49 | this.refresh(nextProps); 50 | } 51 | 52 | componentWillUnmount() { 53 | const { viewItems } = this.state; 54 | viewItems.forEach(item => clearTimeout(item.cacheTimer)); 55 | 56 | Cache.options.hooks.beforeSwitch(this.state.activeId, ''); 57 | Cache.options.hooks.afterSwitch(this.state.activeId, ''); 58 | 59 | getIds(viewItems).forEach(id => { 60 | Cache.options.hooks.beforeRemove(id); 61 | Cache.options.hooks.afterRemove(id); 62 | }) 63 | } 64 | 65 | componentWillUpdate(nextProps, nextState) { 66 | const oldId = this.state.activeId; 67 | const newId = nextState.activeId; 68 | const oldIds = getIds(this.state.viewItems); 69 | const newIds = getIds(nextState.viewItems); 70 | const addedIds = difference(newIds, oldIds); 71 | const removedIds = difference(oldIds, newIds); 72 | 73 | if (oldId !== newId) Cache.options.hooks.beforeSwitch(oldId, newId); 74 | addedIds.forEach(id => Cache.options.hooks.beforeAdd(id)); 75 | removedIds.forEach(id => Cache.options.hooks.beforeRemove(id)); 76 | } 77 | 78 | componentDidUpdate(prevProps, prevState) { 79 | const oldId = prevState.activeId; 80 | const newId = this.state.activeId; 81 | const oldIds = getIds(prevState.viewItems); 82 | const newIds = getIds(this.state.viewItems); 83 | const addedIds = difference(newIds, oldIds); 84 | const removedIds = difference(oldIds, newIds); 85 | 86 | if (oldId !== newId) Cache.options.hooks.afterSwitch(oldId, newId); 87 | addedIds.forEach(id => Cache.options.hooks.afterAdd(id)); 88 | removedIds.forEach(id => Cache.options.hooks.afterRemove(id)); 89 | } 90 | 91 | render() { 92 | const { activeId, viewItems } = this.state; 93 | 94 | return ( 95 |
96 | { 97 | viewItems.map(item => { 98 | const isActive = item.id === activeId; 99 | return
100 | { 101 | typeof item.view === 'function' ? 102 | : item.view 103 | } 104 |
105 | }) 106 | } 107 |
108 | ) 109 | } 110 | 111 | refresh(props) { 112 | const { viewId, view, cacheTime } = props; 113 | const { activeId, viewItems } = this.state; 114 | 115 | const newViewItems = viewItems.slice(); 116 | 117 | if (!viewId) { 118 | // set cache timer for switch-out item 119 | const preViewItem = newViewItems.find(item => item.id === activeId); 120 | this.setCacheTimer(preViewItem); 121 | } 122 | else { 123 | const viewItem = newViewItems.find(item => item.id === viewId); 124 | if (viewItem) { 125 | // update item 126 | viewItem.view = view; 127 | viewItem.cacheTime = cacheTime; 128 | viewItem.lastActivatedAt = new Date; 129 | 130 | if (viewId !== activeId) { 131 | // remove cache timer for switch-in item 132 | clearTimeout(viewItem.cacheTimer); 133 | 134 | // set cache timer for switch-out item 135 | const preViewItem = newViewItems.find(item => item.id === activeId); 136 | this.setCacheTimer(preViewItem) 137 | } 138 | } 139 | else { 140 | // create new item 141 | const newViewItem = { 142 | id: viewId, 143 | view: view, 144 | cacheTime: cacheTime, 145 | lastActivatedAt: new Date, 146 | }; 147 | newViewItems.push(newViewItem); 148 | 149 | // set cache timer for previous item 150 | const preViewItem = newViewItems.find(item => item.id === activeId); 151 | this.setCacheTimer(preViewItem); 152 | 153 | // remove oldest surplus item 154 | if (newViewItems.length > this.getCacheLimit()) { 155 | const oldestItem = newViewItems.reduce( 156 | (min, cur) => cur.lastActivatedAt < min.lastActivatedAt ? cur : min, 157 | newViewItems[ 0 ] 158 | ); 159 | if (oldestItem) { 160 | clearTimeout(oldestItem.cacheTimer); 161 | newViewItems.splice(newViewItems.findIndex(item => item === oldestItem), 1); 162 | } 163 | } 164 | } 165 | } 166 | 167 | // update state 168 | this.setState({ 169 | activeId: viewId, 170 | viewItems: newViewItems, 171 | }) 172 | } 173 | 174 | expireItem(id) { 175 | const { viewItems } = this.state; 176 | const viewItem = viewItems.find(item => item.id === id); 177 | if (viewItem) { 178 | clearTimeout(viewItem.cacheTimer); 179 | const newViewItems = viewItems.filter(item => item !== viewItem); 180 | this.setState({ viewItems: newViewItems }); 181 | } 182 | } 183 | 184 | setCacheTimer(item) { 185 | if (item) { 186 | clearTimeout(item.cacheTimer); 187 | const cacheTime = this.getCacheTime(item); 188 | item.cacheTimer = setTimeout(() => this.expireItem(item.id), cacheTime); 189 | } 190 | } 191 | 192 | getCacheTime(item) { 193 | if (item.cacheTime !== undefined) return item.cacheTime; 194 | else return Cache.options.cacheTime; 195 | } 196 | 197 | getCacheLimit() { 198 | return Cache.options.cacheLimit; 199 | } 200 | } 201 | 202 | // optimize re-render 203 | class View extends React.Component { 204 | static propTypes = { 205 | args: PropTypes.object, 206 | view: PropTypes.func.isRequired, 207 | }; 208 | 209 | shouldComponentUpdate(nextProps) { 210 | return !deepEqual(this.props, nextProps); 211 | } 212 | 213 | render() { 214 | const { args, view } = this.props; 215 | return view(args); 216 | } 217 | } 218 | 219 | return Cache; 220 | } 221 | -------------------------------------------------------------------------------- /src/lib.js: -------------------------------------------------------------------------------- 1 | export function deepEqual(a, b) { 2 | if (!isObject(a) || !isObject(b)) return a === b; 3 | else return deepEqualInner(a, b) && deepEqualInner(b, a); 4 | } 5 | 6 | function deepEqualInner(a, b) { 7 | for (let key in a) { 8 | if (!deepEqual(a[ key ], b[ key ])) return false; 9 | } 10 | return true; 11 | } 12 | 13 | function isObject(x) { 14 | return typeof x === 'object' && x !== null; 15 | } 16 | 17 | export function deepAssign(base, extension) { 18 | if (!isObject(base)) base = {}; 19 | if (!isObject(extension)) extension = {}; 20 | 21 | for (let key in extension) { 22 | if (!isObject(extension[ key ])) base[ key ] = extension[ key ]; 23 | else base[ key ] = deepAssign(base[ key ], extension[ key ]); 24 | } 25 | 26 | return base; 27 | } 28 | 29 | export function getIds(items) { 30 | return items.map(item => item.id); 31 | } 32 | 33 | export function difference(a, b) { 34 | const result = []; 35 | for (let value of a) { 36 | if (b.indexOf(value) < 0) result.push(value); 37 | } 38 | return result; 39 | } -------------------------------------------------------------------------------- /test/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-0"] 3 | } -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test 5 | 6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "version": "0.0.0", 4 | "description": "", 5 | "scripts": { 6 | "build": "webpack --progress --colors" 7 | }, 8 | "author": "zhaoyao91", 9 | "license": "MIT", 10 | "devDependencies": { 11 | "babel-core": "^6.17.0", 12 | "babel-loader": "^6.2.5", 13 | "babel-preset-es2015": "^6.16.0", 14 | "babel-preset-react": "^6.16.0", 15 | "babel-preset-stage-0": "^6.16.0", 16 | "react": "^15.3.2", 17 | "react-dom": "^15.3.2", 18 | "webpack": "^1.13.2" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/src/entry.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom"; 2 | import React from "react"; 3 | import createCache from "react-view-cache"; 4 | 5 | const Cache = createCache({ 6 | hooks: { 7 | beforeSwitch(oldId, newId) { 8 | console.log('before switch', oldId, newId); 9 | }, 10 | 11 | afterSwitch(oldId, newId) { 12 | console.log('after switch', oldId, newId) 13 | }, 14 | 15 | beforeAdd(id) { 16 | console.log('before add', id); 17 | }, 18 | 19 | afterAdd(id) { 20 | console.log('after add', id); 21 | }, 22 | 23 | beforeRemove(id) { 24 | console.log('before remove', id); 25 | }, 26 | 27 | afterRemove(id) { 28 | console.log('after remove', id); 29 | } 30 | } 31 | }); 32 | 33 | class Page extends React.Component { 34 | state = { 35 | text: '', 36 | display: true 37 | }; 38 | 39 | render() { 40 | const { text, display } = this.state; 41 | 42 | return
43 |
44 | 45 | 46 |
47 | 48 | 49 | 50 | {display && } cacheTime={3000}/>} 51 |
52 | } 53 | 54 | onSubmit(e) { 55 | e.preventDefault(); 56 | this.setState({ text: this.refs.input.value }); 57 | } 58 | } 59 | 60 | class View extends React.Component { 61 | componentDidMount() { 62 | // console.log('mount', this.props.text); 63 | } 64 | 65 | componentWillUnmount() { 66 | // console.log('unmount', this.props.text); 67 | } 68 | 69 | render() { 70 | const { text, isActive } = this.props; 71 | 72 | // console.log('render', text, isActive); 73 | 74 | return
75 |
text: {text}
76 |
isActive: {isActive ? 'true' : 'false'}
77 |
78 | } 79 | } 80 | 81 | ReactDOM.render( 82 | , 83 | document.getElementById('root') 84 | ); -------------------------------------------------------------------------------- /test/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | entry: "./src/entry.js", 5 | output: { 6 | path: __dirname, 7 | filename: "./dist/bundle.js" 8 | }, 9 | module: { 10 | loaders: [ 11 | { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" } 12 | ] 13 | }, 14 | resolve: { 15 | root: [ 16 | path.resolve(__dirname, '../../'), 17 | ] 18 | }, 19 | resolveLoader: { 20 | root: [ 21 | path.resolve(__dirname, './node_modules') 22 | ] 23 | }, 24 | devtool: 'source-map' 25 | }; --------------------------------------------------------------------------------