From f5089953b9614315b1ac1f28d1f9f6869ae51bec Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 24 Jul 2018 13:37:14 +0100 Subject: [PATCH] Data module: Add built-in support for persisting stores (#8146) * Data module: Add built-in support for persisting stores * Remove useless storage key * Don't repersist if the persisted changes are kept identical * Restoring deprecated APIs * Avoid using window.localStorage when loading the persist file * Fix inline-tokens registration * Fix with-history Higher-order reducer to be stateless --- edit-post/index.js | 9 +- edit-post/store/index.js | 13 +- editor/store/index.js | 22 +-- editor/utils/with-history/index.js | 34 ++-- editor/utils/with-history/test/index.js | 27 ++- packages/data/src/deprecated.js | 91 ++++++++++ packages/data/src/index.js | 4 +- packages/data/src/persist.js | 92 +++++----- packages/data/src/registry.js | 75 ++++++++- packages/data/src/test/persist.js | 212 +++++++++++++----------- packages/nux/src/store/index.js | 8 +- 11 files changed, 393 insertions(+), 194 deletions(-) create mode 100644 packages/data/src/deprecated.js diff --git a/edit-post/index.js b/edit-post/index.js index ebb223771a3576..0dcf10823ba137 100644 --- a/edit-post/index.js +++ b/edit-post/index.js @@ -3,7 +3,7 @@ */ import { registerCoreBlocks } from '@wordpress/core-blocks'; import { render, unmountComponentAtNode } from '@wordpress/element'; -import { dispatch } from '@wordpress/data'; +import { dispatch, setupPersistence } from '@wordpress/data'; import deprecated from '@wordpress/deprecated'; /** @@ -15,6 +15,11 @@ import store from './store'; import { initializeMetaBoxState } from './store/actions'; import Editor from './editor'; +/** + * Module Constants + */ +const STORAGE_KEY = `WP_EDIT_POST_DATA_${ window.userSettings.uid }`; + /** * Reinitializes the editor after the user chooses to reboot the editor after * an unhandled error occurs, replacing previously mounted editor element using @@ -88,3 +93,5 @@ export { default as PluginPostStatusInfo } from './components/sidebar/plugin-pos export { default as PluginPrePublishPanel } from './components/sidebar/plugin-pre-publish-panel'; export { default as PluginSidebar } from './components/sidebar/plugin-sidebar'; export { default as PluginSidebarMoreMenuItem } from './components/header/plugin-sidebar-more-menu-item'; + +setupPersistence( STORAGE_KEY ); diff --git a/edit-post/store/index.js b/edit-post/store/index.js index d275e8ed209c87..a7160cdf806c03 100644 --- a/edit-post/store/index.js +++ b/edit-post/store/index.js @@ -3,8 +3,7 @@ */ import { registerStore, - withRehydration, - loadAndPersist, + restrictPersistence, } from '@wordpress/data'; /** @@ -14,20 +13,14 @@ import reducer from './reducer'; import applyMiddlewares from './middlewares'; import * as actions from './actions'; import * as selectors from './selectors'; - -/** - * Module Constants - */ -const STORAGE_KEY = `WP_EDIT_POST_PREFERENCES_${ window.userSettings.uid }`; - const store = registerStore( 'core/edit-post', { - reducer: withRehydration( reducer, 'preferences', STORAGE_KEY ), + reducer: restrictPersistence( reducer, 'preferences' ), actions, selectors, + persist: true, } ); applyMiddlewares( store ); -loadAndPersist( store, reducer, 'preferences', STORAGE_KEY ); store.dispatch( { type: 'INIT' } ); export default store; diff --git a/editor/store/index.js b/editor/store/index.js index 44f3c446b4aabc..90a1ec42ccf884 100644 --- a/editor/store/index.js +++ b/editor/store/index.js @@ -7,11 +7,8 @@ import { forOwn } from 'lodash'; * WordPress Dependencies */ import { - registerReducer, - registerSelectors, - registerActions, - withRehydration, - loadAndPersist, + registerStore, + restrictPersistence, } from '@wordpress/data'; /** @@ -27,16 +24,15 @@ import { validateTokenSettings } from '../components/rich-text/tokens'; /** * Module Constants */ -const STORAGE_KEY = `GUTENBERG_PREFERENCES_${ window.userSettings.uid }`; const MODULE_KEY = 'core/editor'; -const store = applyMiddlewares( - registerReducer( MODULE_KEY, withRehydration( reducer, 'preferences', STORAGE_KEY ) ) -); -loadAndPersist( store, reducer, 'preferences', STORAGE_KEY ); - -registerSelectors( MODULE_KEY, selectors ); -registerActions( MODULE_KEY, actions ); +const store = registerStore( MODULE_KEY, { + reducer: restrictPersistence( reducer, 'preferences' ), + selectors, + actions, + persist: true, +} ); +applyMiddlewares( store ); forOwn( tokens, ( { name, settings } ) => { settings = validateTokenSettings( name, settings, store.getState() ); diff --git a/editor/utils/with-history/index.js b/editor/utils/with-history/index.js index cc90f0d4b66478..9a6bf045df94c7 100644 --- a/editor/utils/with-history/index.js +++ b/editor/utils/with-history/index.js @@ -47,22 +47,19 @@ const withHistory = ( options = {} ) => ( reducer ) => { past: [], present: reducer( undefined, {} ), future: [], + lastAction: null, + shouldCreateUndoLevel: false, }; - let lastAction; - let shouldCreateUndoLevel = false; - const { resetTypes = [], shouldOverwriteState = () => false, } = options; return ( state = initialState, action ) => { - const { past, present, future } = state; + const { past, present, future, lastAction, shouldCreateUndoLevel } = state; const previousAction = lastAction; - lastAction = action; - switch ( action.type ) { case 'UNDO': // Can't undo if no past. @@ -74,6 +71,8 @@ const withHistory = ( options = {} ) => ( reducer ) => { past: dropRight( past ), present: last( past ), future: [ present, ...future ], + lastAction: null, + shouldCreateUndoLevel: false, }; case 'REDO': // Can't redo if no future. @@ -85,11 +84,16 @@ const withHistory = ( options = {} ) => ( reducer ) => { past: [ ...past, present ], present: first( future ), future: drop( future ), + lastAction: null, + shouldCreateUndoLevel: false, }; case 'CREATE_UNDO_LEVEL': - shouldCreateUndoLevel = true; - return state; + return { + ...state, + lastAction: null, + shouldCreateUndoLevel: true, + }; } const nextPresent = reducer( present, action ); @@ -99,6 +103,8 @@ const withHistory = ( options = {} ) => ( reducer ) => { past: [], present: nextPresent, future: [], + lastAction: null, + shouldCreateUndoLevel: false, }; } @@ -108,18 +114,20 @@ const withHistory = ( options = {} ) => ( reducer ) => { let nextPast = past; - shouldCreateUndoLevel = ! past.length || shouldCreateUndoLevel; - - if ( shouldCreateUndoLevel || ! shouldOverwriteState( action, previousAction ) ) { + if ( + shouldCreateUndoLevel || + ! past.length || + ! shouldOverwriteState( action, previousAction ) + ) { nextPast = [ ...past, present ]; } - shouldCreateUndoLevel = false; - return { past: nextPast, present: nextPresent, future: [], + shouldCreateUndoLevel: false, + lastAction: action, }; }; }; diff --git a/editor/utils/with-history/test/index.js b/editor/utils/with-history/test/index.js index 10757a09471468..505f1a17595e0d 100644 --- a/editor/utils/with-history/test/index.js +++ b/editor/utils/with-history/test/index.js @@ -16,6 +16,8 @@ describe( 'withHistory', () => { past: [], present: 0, future: [], + lastAction: null, + shouldCreateUndoLevel: false, } ); } ); @@ -23,21 +25,26 @@ describe( 'withHistory', () => { const reducer = withHistory()( counter ); let state; + const action = { type: 'INCREMENT' }; state = reducer( undefined, {} ); - state = reducer( state, { type: 'INCREMENT' } ); + state = reducer( state, action ); expect( state ).toEqual( { past: [ 0 ], present: 1, future: [], + lastAction: action, + shouldCreateUndoLevel: false, } ); - state = reducer( state, { type: 'INCREMENT' } ); + state = reducer( state, action ); expect( state ).toEqual( { past: [ 0, 1 ], present: 2, future: [], + lastAction: action, + shouldCreateUndoLevel: false, } ); } ); @@ -53,6 +60,8 @@ describe( 'withHistory', () => { past: [], present: 0, future: [ 1 ], + lastAction: null, + shouldCreateUndoLevel: false, } ); } ); @@ -76,6 +85,8 @@ describe( 'withHistory', () => { past: [ 0 ], present: 1, future: [], + lastAction: null, + shouldCreateUndoLevel: false, } ); } ); @@ -98,6 +109,8 @@ describe( 'withHistory', () => { past: [], present: 1, future: [], + lastAction: null, + shouldCreateUndoLevel: false, } ); } ); @@ -113,6 +126,8 @@ describe( 'withHistory', () => { past: [ 0 ], // Needs at least one history present: 2, future: [], + lastAction: { type: 'INCREMENT' }, + shouldCreateUndoLevel: false, } ); } ); @@ -137,6 +152,8 @@ describe( 'withHistory', () => { past: [ 0 ], present: 1, future: [], + lastAction: { type: 'INCREMENT' }, + shouldCreateUndoLevel: false, } ); state = reducer( state, { type: 'INCREMENT' } ); @@ -145,6 +162,8 @@ describe( 'withHistory', () => { past: [ 0 ], present: 2, future: [], + lastAction: { type: 'INCREMENT' }, + shouldCreateUndoLevel: false, } ); } ); @@ -162,6 +181,8 @@ describe( 'withHistory', () => { past: [ 0 ], present: 1, future: [], + lastAction: null, + shouldCreateUndoLevel: true, } ); state = reducer( state, { type: 'INCREMENT' } ); @@ -170,6 +191,8 @@ describe( 'withHistory', () => { past: [ 0, 1 ], present: 2, future: [], + lastAction: { type: 'INCREMENT' }, + shouldCreateUndoLevel: false, } ); } ); } ); diff --git a/packages/data/src/deprecated.js b/packages/data/src/deprecated.js new file mode 100644 index 00000000000000..5bfdb1f0d58d26 --- /dev/null +++ b/packages/data/src/deprecated.js @@ -0,0 +1,91 @@ +/** + * External dependencies + */ +import { get } from 'lodash'; + +/** + * WordPress dependencies + */ +import deprecated from '@wordpress/deprecated'; + +/** + * Internal dependencies + */ +import { getPersistenceStorage } from './persist'; + +/** + * Adds the rehydration behavior to redux reducers. + * + * @param {Function} reducer The reducer to enhance. + * @param {string} reducerKey The reducer key to persist. + * @param {string} storageKey The storage key to use. + * + * @return {Function} Enhanced reducer. + */ +export function withRehydration( reducer, reducerKey, storageKey ) { + deprecated( 'wp.data.withRehydration', { + version: '3.6', + plugin: 'Gutenberg', + hint: 'See https://github.com/WordPress/gutenberg/pull/8146 for more details', + } ); + + // EnhancedReducer with auto-rehydration + const enhancedReducer = ( state, action ) => { + const nextState = reducer( state, action ); + + if ( action.type === 'REDUX_REHYDRATE' && action.storageKey === storageKey ) { + return { + ...nextState, + [ reducerKey ]: action.payload, + }; + } + + return nextState; + }; + + return enhancedReducer; +} + +/** + * Loads the initial state and persist on changes. + * + * This should be executed after the reducer's registration. + * + * @param {Object} store Store to enhance. + * @param {Function} reducer The reducer function. Used to get default values and to allow custom serialization by the reducers. + * @param {string} reducerKey The reducer key to persist (example: reducerKey.subReducerKey). + * @param {string} storageKey The storage key to use. + */ +export function loadAndPersist( store, reducer, reducerKey, storageKey ) { + deprecated( 'wp.data.loadAndPersist', { + version: '3.6', + plugin: 'Gutenberg', + hint: 'See https://github.com/WordPress/gutenberg/pull/8146 for more details', + } ); + + // Load initially persisted value + const persistedString = getPersistenceStorage().getItem( storageKey ); + if ( persistedString ) { + const persistedState = { + ...get( reducer( undefined, { type: '@@gutenberg/init' } ), reducerKey ), + ...JSON.parse( persistedString ), + }; + + store.dispatch( { + type: 'REDUX_REHYDRATE', + payload: persistedState, + storageKey, + } ); + } + + // Persist updated preferences + let currentStateValue = get( store.getState(), reducerKey ); + store.subscribe( () => { + const newStateValue = get( store.getState(), reducerKey ); + if ( newStateValue !== currentStateValue ) { + currentStateValue = newStateValue; + const stateToSave = get( reducer( store.getState(), { type: 'SERIALIZE' } ), reducerKey ); + getPersistenceStorage().setItem( storageKey, JSON.stringify( stateToSave ) ); + } + } ); +} diff --git a/packages/data/src/index.js b/packages/data/src/index.js index c556c9bd81a389..3cbd9aa941052a 100644 --- a/packages/data/src/index.js +++ b/packages/data/src/index.js @@ -7,11 +7,12 @@ import { combineReducers } from 'redux'; * Internal dependencies */ import defaultRegistry from './default-registry'; -export { loadAndPersist, withRehydration } from './persist'; +export { restrictPersistence, setPersistenceStorage } from './persist'; export { default as withSelect } from './components/with-select'; export { default as withDispatch } from './components/with-dispatch'; export { default as RegistryProvider } from './components/registry-provider'; export { createRegistry } from './registry'; +export { withRehydration, loadAndPersist } from './deprecated'; /** * The combineReducers helper function turns an object whose values are different @@ -33,3 +34,4 @@ export const registerReducer = defaultRegistry.registerReducer; export const registerActions = defaultRegistry.registerActions; export const registerSelectors = defaultRegistry.registerSelectors; export const registerResolvers = defaultRegistry.registerResolvers; +export const setupPersistence = defaultRegistry.setupPersistence; diff --git a/packages/data/src/persist.js b/packages/data/src/persist.js index b8c850674d0d9d..9ec3b029b4c537 100644 --- a/packages/data/src/persist.js +++ b/packages/data/src/persist.js @@ -1,10 +1,5 @@ -/** - * External dependencies - */ -import { get } from 'lodash'; - // Defaults to the local storage. -let persistenceStorage = window.localStorage; +let persistenceStorage; /** * Sets a different persistence storage. @@ -15,67 +10,74 @@ export function setPersistenceStorage( storage ) { persistenceStorage = storage; } +/** + * Get the persistence storage handler. + * + * @return {Object} Persistence storage. + */ +export function getPersistenceStorage() { + return persistenceStorage || window.localStorage; +} + /** * Adds the rehydration behavior to redux reducers. * * @param {Function} reducer The reducer to enhance. - * @param {string} reducerKey The reducer key to persist. * @param {string} storageKey The storage key to use. * * @return {Function} Enhanced reducer. */ -export function withRehydration( reducer, reducerKey, storageKey ) { +export function withRehydration( reducer ) { // EnhancedReducer with auto-rehydration const enhancedReducer = ( state, action ) => { - const nextState = reducer( state, action ); - - if ( action.type === 'REDUX_REHYDRATE' && action.storageKey === storageKey ) { - return { - ...nextState, - [ reducerKey ]: action.payload, - }; + if ( action.type === 'REDUX_REHYDRATE' ) { + return reducer( action.payload, { + ...action, + previousState: state, + } ); } - return nextState; + return reducer( state, action ); }; return enhancedReducer; } /** - * Loads the initial state and persist on changes. + * Higher-order reducer used to persist just one key from the reducer state. * - * This should be executed after the reducer's registration. + * @param {function} reducer Reducer function. + * @param {string} keyToPersist The reducer key to persist. * - * @param {Object} store Store to enhance. - * @param {Function} reducer The reducer function. Used to get default values and to allow custom serialization by the reducers. - * @param {string} reducerKey The reducer key to persist (example: reducerKey.subReducerKey). - * @param {string} storageKey The storage key to use. + * @return {function} Updated reducer. */ -export function loadAndPersist( store, reducer, reducerKey, storageKey ) { - // Load initially persisted value - const persistedString = persistenceStorage.getItem( storageKey ); - if ( persistedString ) { - const persistedState = { - ...get( reducer( undefined, { type: '@@gutenberg/init' } ), reducerKey ), - ...JSON.parse( persistedString ), - }; +export function restrictPersistence( reducer, keyToPersist ) { + return ( state, action ) => { + const nextState = reducer( state, action ); - store.dispatch( { - type: 'REDUX_REHYDRATE', - payload: persistedState, - storageKey, - } ); - } + if ( action.type === 'SERIALIZE' ) { + // Returning the same instance if the state is kept identical avoids reserializing again + if ( + action.previousState && + action.previousState[ keyToPersist ] === nextState[ keyToPersist ] + ) { + return action.previousState; + } - // Persist updated preferences - let currentStateValue = get( store.getState(), reducerKey ); - store.subscribe( () => { - const newStateValue = get( store.getState(), reducerKey ); - if ( newStateValue !== currentStateValue ) { - currentStateValue = newStateValue; - const stateToSave = get( reducer( store.getState(), { type: 'SERIALIZE' } ), reducerKey ); - persistenceStorage.setItem( storageKey, JSON.stringify( stateToSave ) ); + return { [ keyToPersist ]: nextState[ keyToPersist ] }; } - } ); + + if ( action.type === 'REDUX_REHYDRATE' ) { + return { + ...action.previousState, + ...state, + [ keyToPersist ]: { + ...action.previousState[ keyToPersist ], + ...state[ keyToPersist ], + }, + }; + } + + return nextState; + }; } diff --git a/packages/data/src/registry.js b/packages/data/src/registry.js index 5275329d26ba4a..890b5ae43f01ac 100644 --- a/packages/data/src/registry.js +++ b/packages/data/src/registry.js @@ -4,10 +4,16 @@ import { createStore } from 'redux'; import { flowRight, without, mapValues, overEvery, get } from 'lodash'; +/** + * WordPress dependencies + */ +import isShallowEqual from '@wordpress/is-shallow-equal'; + /** * Internal dependencies */ import dataStore from './store'; +import { withRehydration, getPersistenceStorage } from './persist'; /** * Returns true if the given argument appears to be a dispatchable action. @@ -95,18 +101,20 @@ export function createRegistry( storeConfigs = {} ) { /** * Registers a new sub-reducer to the global state and returns a Redux-like store object. * - * @param {string} reducerKey Reducer key. - * @param {Object} reducer Reducer function. + * @param {string} reducerKey Reducer key. + * @param {Object} reducer Reducer function. + * @param {boolean} persist Should the reducer be persisted. * * @return {Object} Store Object. */ - function registerReducer( reducerKey, reducer ) { + function registerReducer( reducerKey, reducer, persist = false ) { const enhancers = []; if ( window.__REDUX_DEVTOOLS_EXTENSION__ ) { enhancers.push( window.__REDUX_DEVTOOLS_EXTENSION__( { name: reducerKey, instanceId: reducerKey } ) ); } + reducer = persist ? withRehydration( reducer ) : reducer; const store = createStore( reducer, flowRight( enhancers ) ); - namespaces[ reducerKey ] = { store }; + namespaces[ reducerKey ] = { store, reducer, persist }; // Customize subscribe behavior to call listeners only on effective change, // not on every dispatch. @@ -242,7 +250,7 @@ export function createRegistry( storeConfigs = {} ) { throw new TypeError( 'Must specify store reducer' ); } - const store = registerReducer( reducerKey, options.reducer ); + const store = registerReducer( reducerKey, options.reducer, options.persist ); if ( options.actions ) { registerActions( reducerKey, options.actions ); @@ -298,6 +306,62 @@ export function createRegistry( storeConfigs = {} ) { return get( namespaces, [ reducerKey, 'actions' ] ); } + /** + * Setup persistence for the current registry. + * + * @param {string} storageKey The storage key. + */ + function setupPersistence( storageKey ) { + const persistenceStorage = getPersistenceStorage(); + + // Load initially persisted value + let previousValue = null; + const persistedString = persistenceStorage.getItem( storageKey ); + if ( persistedString ) { + const persistedData = JSON.parse( persistedString ); + Object.entries( namespaces ).forEach( ( [ reducerKey, { store, persist } ] ) => { + if ( ! persist ) { + return; + } + + const persistedState = { + ...store.getState(), + ...get( persistedData, reducerKey ), + }; + + store.dispatch( { + type: 'REDUX_REHYDRATE', + payload: persistedState, + } ); + } ); + + // Avoid initial save. + previousValue = persistedData; + } + + const triggerPersist = () => { + const newValue = Object.entries( namespaces ) + .filter( ( [ , { persist } ] ) => persist ) + .reduce( ( memo, [ reducerKey, { reducer, store } ] ) => { + memo[ reducerKey ] = reducer( store.getState(), { + type: 'SERIALIZE', + previousState: get( previousValue, reducerKey ), + } ); + return memo; + }, {} ); + + if ( ! isShallowEqual( newValue, previousValue ) ) { + persistenceStorage.setItem( storageKey, JSON.stringify( newValue ) ); + } + + previousValue = newValue; + }; + + // Persist updated preferences + subscribe( triggerPersist ); + triggerPersist(); + } + Object.entries( { 'core/data': dataStore, ...storeConfigs, @@ -312,5 +376,6 @@ export function createRegistry( storeConfigs = {} ) { subscribe, select, dispatch, + setupPersistence, }; } diff --git a/packages/data/src/test/persist.js b/packages/data/src/test/persist.js index dae9de384b9fa5..4a55ae18905e79 100644 --- a/packages/data/src/test/persist.js +++ b/packages/data/src/test/persist.js @@ -1,50 +1,34 @@ -/** - * External dependencies - */ -import { createStore } from 'redux'; - /** * Internal dependencies */ -import { loadAndPersist, withRehydration } from '../persist'; - -describe( 'loadAndPersist', () => { - const persistenceStorage = window.localStorage; +import { getPersistenceStorage, setPersistenceStorage, restrictPersistence } from '../persist'; +import { createRegistry } from '../registry'; + +describe( 'persiss registry', () => { + let registry; + beforeEach( () => { + registry = createRegistry(); + setPersistenceStorage( window.localStorage ); + } ); it( 'should load the initial value from the local storage integrating it into reducer default value.', () => { const storageKey = 'dumbStorageKey'; - persistenceStorage.setItem( storageKey, JSON.stringify( { chicken: true } ) ); - const reducer = () => { - return { - preferences: { ribs: true }, - }; - }; - const store = createStore( withRehydration( reducer, 'preferences', storageKey ) ); - loadAndPersist( - store, - reducer, - 'preferences', - storageKey, - ); - expect( store.getState().preferences ).toEqual( { chicken: true, ribs: true } ); - } ); + const store = registry.registerStore( 'storeKey', { + reducer: ( state = { ribs: true } ) => { + return state; + }, + persist: true, + } ); - it( 'should not load the initial value from the local storage if the storage key is different.', () => { - const storageKey = 'dumbStorageKey'; - persistenceStorage.setItem( storageKey, JSON.stringify( { chicken: true } ) ); - const reducer = () => { - return { - preferences: { ribs: true }, - }; - }; - const store = createStore( withRehydration( reducer, 'preferences', storageKey + 'change' ) ); - loadAndPersist( - store, - reducer, - 'preferences', - storageKey, - ); - expect( store.getState().preferences ).toEqual( { ribs: true } ); + getPersistenceStorage().setItem( storageKey, JSON.stringify( { + storeKey: { + chicken: true, + }, + } ) ); + + registry.setupPersistence( storageKey ); + + expect( store.getState() ).toEqual( { chicken: true, ribs: true } ); } ); it( 'should persist to local storage once the state value changes', () => { @@ -55,83 +39,113 @@ describe( 'loadAndPersist', () => { } if ( action.type === 'UPDATE' ) { - return { - preferences: { chicken: true }, - }; + return { chicken: true }; } - return { - preferences: { ribs: true }, - }; + return { ribs: true }; }; - const store = createStore( withRehydration( reducer, 'preferences', storageKey ) ); - loadAndPersist( - store, + const store = registry.registerStore( 'storeKey', { reducer, - 'preferences', - storageKey, - ); + persist: true, + } ); + + registry.setupPersistence( storageKey ); + store.dispatch( { type: 'UPDATE' } ); - expect( JSON.parse( persistenceStorage.getItem( storageKey ) ) ).toEqual( { chicken: true } ); + expect( JSON.parse( getPersistenceStorage().getItem( storageKey ) ) ) + .toEqual( { storeKey: { chicken: true } } ); } ); - it( 'should apply defaults to any missing properties on previously stored objects', () => { - const defaultsPreferences = { - counter: 41, + it( 'should not trigger persistence if the value doesn\'t change', () => { + const storageKey = 'dumbStorageKey2'; + let countCalls = 0; + const storage = { + getItem() { + return this.item; + }, + setItem( key, value ) { + countCalls++; + this.item = value; + }, }; - const storageKey = 'dumbStorageKey3'; - const reducer = ( state = { preferences: defaultsPreferences }, action ) => { - if ( action.type === 'INCREMENT' ) { - return { - preferences: { counter: state.preferences.counter + 1 }, - }; + setPersistenceStorage( storage ); + const reducer = ( state = { ribs: true }, action ) => { + if ( action.type === 'UPDATE' ) { + return { chicken: true }; } + return state; }; + registry.registerStore( 'store1', { + reducer, + persist: true, + actions: { + update: () => ( { type: 'UPDATE' } ), + }, + } ); + registry.registerStore( 'store2', { + reducer, + actions: { + update: () => ( { type: 'UPDATE' } ), + }, + } ); + registry.setupPersistence( storageKey ); - // store preferences without the `counter` default - persistenceStorage.setItem( storageKey, JSON.stringify( {} ) ); + expect( countCalls ).toBe( 1 ); // Setup trigger initial persistence. - const store = createStore( withRehydration( reducer, 'preferences', storageKey ) ); - loadAndPersist( - store, - reducer, - 'preferences', - storageKey, - ); - store.dispatch( { type: 'INCREMENT' } ); - - // the default should have been applied, as the `counter` was missing from the - // saved preferences, then the INCREMENT action should have taken effect to give us 42 - expect( JSON.parse( persistenceStorage.getItem( storageKey ) ) ).toEqual( { counter: 42 } ); + registry.dispatch( 'store1' ).update(); + + expect( countCalls ).toBe( 2 ); // Updating state trigger persistence. + + registry.dispatch( 'store2' ).update(); + + expect( countCalls ).toBe( 2 ); // If the persisted state doesn't change, don't persist. } ); +} ); - it( 'should not override stored values with defaults', () => { - const defaultsPreferences = { - counter: 41, - }; - const storageKey = 'dumbStorageKey4'; - const reducer = ( state = { preferences: defaultsPreferences }, action ) => { - if ( action.type === 'INCREMENT' ) { - return { - preferences: { counter: state.preferences.counter + 1 }, - }; - } - return state; - }; +describe( 'restrictPersistence', () => { + it( 'should only serialize a sub reducer state', () => { + const reducer = restrictPersistence( () => { + return { + preferences: { + chicken: 'ribs', + }, - persistenceStorage.setItem( storageKey, JSON.stringify( { counter: 1 } ) ); + a: 'b', + }; + }, 'preferences' ); - const store = createStore( withRehydration( reducer, 'preferences', storageKey ) ); + expect( reducer( undefined, { type: 'SERIALIZE' } ) ).toEqual( { + preferences: { + chicken: 'ribs', + }, + } ); + } ); - loadAndPersist( - store, - reducer, - 'preferences', - storageKey, - ); - store.dispatch( { type: 'INCREMENT' } ); + it( 'should merge the substate with the default value', () => { + const reducer = restrictPersistence( () => { + return { + preferences: { + chicken: true, + }, - expect( JSON.parse( persistenceStorage.getItem( storageKey ) ) ).toEqual( { counter: 2 } ); + a: 'b', + }; + }, 'preferences' ); + const state = reducer( undefined, { type: '@@init' } ); + expect( reducer( { + preferences: { + ribs: true, + }, + }, { + type: 'REDUX_REHYDRATE', + previousState: state, + } ) ).toEqual( { + a: 'b', + preferences: { + chicken: true, + ribs: true, + }, + } ); } ); } ); diff --git a/packages/nux/src/store/index.js b/packages/nux/src/store/index.js index 3d387c76f8f5fc..6ad84580738875 100644 --- a/packages/nux/src/store/index.js +++ b/packages/nux/src/store/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { registerStore, withRehydration, loadAndPersist } from '@wordpress/data'; +import { registerStore, restrictPersistence } from '@wordpress/data'; /** * Internal dependencies @@ -11,14 +11,12 @@ import * as actions from './actions'; import * as selectors from './selectors'; const REDUCER_KEY = 'preferences'; -const STORAGE_KEY = `GUTENBERG_NUX_${ window.userSettings.uid }`; const store = registerStore( 'core/nux', { - reducer: withRehydration( reducer, REDUCER_KEY, STORAGE_KEY ), + reducer: restrictPersistence( reducer, REDUCER_KEY ), actions, selectors, + persist: true, } ); -loadAndPersist( store, reducer, REDUCER_KEY, STORAGE_KEY ); - export default store;