Skip to content

Commit

Permalink
Perf: Only synchronize storage on unmount.
Browse files Browse the repository at this point in the history
This should save a lot of stringify / ls churn by only saving when we
absolutely must.

Additionally, I've reworked the test suite to be more modern.

This has a semantic change; state is not synced as often and we can
no longer throw errors if you have a key collision, so this will
be a major update.
  • Loading branch information
STRML committed Feb 1, 2018
1 parent e06ed6b commit c5f25b5
Show file tree
Hide file tree
Showing 10 changed files with 2,471 additions and 2,046 deletions.
7 changes: 7 additions & 0 deletions .babelrc
@@ -0,0 +1,7 @@
{
"presets": ['react'],
"plugins": [
"transform-decorators-legacy",
"transform-class-properties",
]
}
6 changes: 4 additions & 2 deletions .eslintrc
@@ -1,9 +1,11 @@
{
root: true,
extends: "eslint:recommended",
extends: [
"eslint:recommended",
],
rules: {
},
env: {
"node": true
node: true,
},
}
3 changes: 3 additions & 0 deletions jest.config.js
@@ -0,0 +1,3 @@
module.exports = {
"setupFiles": ["jest-localstorage-mock"],
}
76 changes: 0 additions & 76 deletions karma.conf.js

This file was deleted.

28 changes: 15 additions & 13 deletions package.json
Expand Up @@ -4,7 +4,7 @@
"description": "A mixin for automatically synchronizing a component's state with localStorage.",
"main": "react-localstorage.js",
"scripts": {
"test": "karma start",
"test": "jest",
"lint": "eslint ."
},
"repository": {
Expand All @@ -22,19 +22,21 @@
},
"homepage": "https://github.com/STRML/react-localstorage",
"devDependencies": {
"browserify": "^14.0.0",
"envify": "~1.2.1",
"eslint": "^3.15.0",
"jasmine-core": "^2.1.3",
"karma": "^1.4.1",
"karma-browserify": "^5.1.1",
"karma-chrome-launcher": "^2.0.0",
"karma-jasmine": "^1.1.0",
"babel-core": "^6.26.0",
"babel-eslint": "^8.2.1",
"babel-jest": "^22.1.0",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-preset-react": "^6.24.1",
"eslint": "^4.16.0",
"eslint-plugin-react": "^7.6.1",
"jest": "^22.1.4",
"jest-localstorage-mock": "^2.2.0",
"precommit": "^1.2.2",
"react": "^15.4.2",
"react-addons-test-utils": "^15.4.2",
"react-dom": "^15.4.2",
"reactify": "^1.1.1"
"react": "^16",
"react-dom": "^16",
"react-mixin": "^4.0.0",
"regenerator-runtime": "^0.11.1"
},
"publishConfig": {
"registry": "https://registry.npmjs.org"
Expand Down
100 changes: 42 additions & 58 deletions react-localstorage.js
@@ -1,19 +1,17 @@
'use strict';
var warn = require('./lib/warning');
var hasLocalStorage = 'localStorage' in global;
var ls, testKey;

if (hasLocalStorage) {
testKey = 'react-localstorage.mixin.test-key';
try {
// Access to global `localStorage` property must be guarded as it
// fails under iOS private session mode.
ls = global.localStorage;
ls.setItem(testKey, 'foo');
ls.removeItem(testKey);
} catch (e) {
hasLocalStorage = false;
}
var hasLocalStorage = true;
var testKey = 'react-localstorage.mixin.test-key';
var ls;
try {
// Access to global `localStorage` property must be guarded as it
// fails under iOS private session mode.
ls = global.localStorage;
ls.setItem(testKey, 'foo');
ls.removeItem(testKey);
} catch (e) {
hasLocalStorage = false;
}

// Warn if localStorage cannot be found or accessed.
Expand All @@ -26,31 +24,17 @@ if (process.browser) {

module.exports = {
/**
* Error checking. On update, ensure that the last state stored in localStorage is equal
* to the state on the component. We skip the check the first time around as state is left
* alone until mount to keep server rendering working.
* On unmount, save data.
*
* If it is not consistent, we know that someone else is modifying localStorage out from under us, so we throw
* an error.
*
* There are a lot of ways this can happen, so it is worth throwing the error.
* If the page unloads, this may not fire, so we also mount the function to onbeforeunload.
*/
componentWillUpdate: function(nextProps, nextState) {
if (!hasLocalStorage || !this.__stateLoadedFromLS) return;
var key = getLocalStorageKey(this);
if (key === false) return;
var prevStoredState = ls.getItem(key);
if (prevStoredState && process.env.NODE_ENV !== "production") {
warn(
prevStoredState === JSON.stringify(getSyncState(this, this.state)),
'While component ' + getDisplayName(this) + ' was saving state to localStorage, ' +
'the localStorage entry was modified by another actor. This can happen when multiple ' +
'components are using the same localStorage key. Set the property `localStorageKey` ' +
'on ' + getDisplayName(this) + '.'
);
componentWillUnmount: function() {
saveStateToLocalStorage(this);

// Remove beforeunload handler if it exists.
if (this.__react_localstorage_beforeunload) {
global.removeEventListener('beforeunload', this.__react_localstorage_beforeunload);
}
// Since setState() can't be called in CWU, it's a fine time to save the state.
ls.setItem(key, JSON.stringify(getSyncState(this, nextState)));
},

/**
Expand All @@ -60,40 +44,39 @@ module.exports = {
* of breaking the checksum and causing a full rerender, we instead change the component after mount
* for an efficient diff.
*/
componentDidMount: function () {
if (!hasLocalStorage) return;
var me = this;
loadStateFromLocalStorage(this, function() {
// After setting state, mirror back to localstorage.
// This prevents invariants if the developer has changed the initial state of the component.
ls.setItem(getLocalStorageKey(me), JSON.stringify(getSyncState(me, me.state)));
});
componentDidMount: function() {
loadStateFromLocalStorage(this);

// We won't get a componentWillUnmount event if we close the tab or refresh, so add a listener
// and synchronously populate LS.
if (hasLocalStorage && this.__react_localstorage_loaded && global.addEventListener) {
this.__react_localstorage_beforeunload = module.exports.componentWillUnmount.bind(this);
global.addEventListener('beforeunload', this.__react_localstorage_beforeunload);
}

}
};

function loadStateFromLocalStorage(component, cb) {
if (!ls) return;
function loadStateFromLocalStorage(component) {
if (!hasLocalStorage) return;
var key = getLocalStorageKey(component);
if (key === false) return;
var settingState = false;
try {
var storedState = JSON.parse(ls.getItem(key));
if (storedState) {
settingState = true;
component.setState(storedState, done);
}
if (storedState) component.setState(storedState);
} catch(e) {
// eslint-disable-next-line no-console
if (console) console.warn("Unable to load state for", getDisplayName(component), "from localStorage.");
}
// If we didn't set state, run the callback right away.
if (!settingState) done();
component.__react_localstorage_loaded = true;
}

function done() {
// Flag this component as loaded.
component.__stateLoadedFromLS = true;
cb();
}

function saveStateToLocalStorage(component) {
if (!hasLocalStorage || !component.__react_localstorage_loaded) return;
var key = getLocalStorageKey(component);
if (key === false) return;
ls.setItem(key, JSON.stringify(getSyncState(component)));
}

function getDisplayName(component) {
Expand Down Expand Up @@ -122,7 +105,8 @@ function getStateFilterKeys(component) {
* Filters state to only save keys defined in stateFilterKeys.
* If stateFilterKeys is not set, returns full state.
*/
function getSyncState(component, state) {
function getSyncState(component) {
var state = component.state;
var stateFilterKeys = getStateFilterKeys(component);
if (!stateFilterKeys || !state) return state;
var result = {}, key;
Expand Down

0 comments on commit c5f25b5

Please sign in to comment.