Polymer / pwa-starter-kit

Starter templates for building full-featured Progressive Web Apps from web components.
https://pwa-starter-kit.polymer-project.org
2.36k stars 431 forks source link

Persistence in LocalStorage: can't rehydrate state from localStorage #228

Closed peter-lyon closed 6 years ago

peter-lyon commented 6 years ago

To understand how redux works and how the state can be saved in localStorage, I am trying to implement a system similar to "Flash Cards", with a new module "localStorage.js". The state is correctly saved in localStorage and does not throw any exceptions when loading the status from localStorage, but all values are initialized to their default value even when hardcoding a JSON.

Any ideas to make this work?

localStorage.js:

const MY_KEY = "yeah"

export const saveState = (state) => {
  localStorage.setItem(MY_KEY, JSON.stringify(state););
}

export const loadState = () => {
  let state = JSON.parse(localStorage.getItem(MY_KEY) || '{}');
  if (state) {
    return state;
  } else {
    return undefined; 
  }
}

store.js:

import {
  createStore,
  compose as origCompose,
  applyMiddleware,
  combineReducers
} from 'redux';
import thunk from 'redux-thunk';
import { lazyReducerEnhancer } from 'pwa-helpers/lazy-reducer-enhancer.js';
import { loadState, saveState } from './localStorage.js';
import app from './reducers/app.js';
const compose = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || origCompose;

export const store = createStore(
  (state, action) => state,
  loadState(),  // If there is local storage data, load it.
  compose(lazyReducerEnhancer(combineReducers), applyMiddleware(thunk))
);

// Initially loaded reducers.
store.addReducers({
  app
});

// This subscriber writes to local storage anytime the state updates.
store.subscribe(() => {
  saveState(store.getState());
});
frankiefu commented 6 years ago

The code you put here is pretty much the same as in flash-cards app, and it works in flash-cards. So I think somewhere else in your app is causing the issue, like resetting the state that you don't expected. Try putting a breakpoint in your reducer and see if you can spot where the state is getting modified incorrectly.

peter-lyon commented 6 years ago

Is not "my code", is your code . Following the documentation-site (https://polymer.github.io/pwa-starter-kit/redux-and-state-management/), in the "Replicating the state for storage" Section, I've modified store.js and I've created localStorage.js, as I showed in the issue description. The state is saved correctly in the reducer, I've verified it. The problem is this line: loadState(), // If there is local storage data, load it.

Don't cause any effects. No exceptions, no logs.

Any idea that can help me? Thanks!

whentotrade commented 5 years ago

I am observing the same results as described by peter. Used the PWA started kit with the basic source from here: https://github.com/Polymer/pwa-starter-kit/tree/template-typescript and using the basic counter example to store the state in local storage for the counter element.

added localStorage.ts:

export const saveState = (state) => {
    let stringifiedState = JSON.stringify(state);
    localStorage.setItem('__wtt_store__', stringifiedState);
  }

export const loadState = () => {
    try {
        let json = localStorage.getItem('__wtt_store__') || '{}';
        let state = JSON.parse(json);
        console.log("local storage state:"+state.counter.clicks);

        if (state) {
        return state;
        } else {
        return undefined;  // To use the defaults in the reducers
        }
    } catch { 
        console.log("local storage error");
        return undefined;
    }
  }

Changed store.ts template to

declare global {
  interface Window {
    process?: Object;
    __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose;
  }
}

import {
  createStore,
  compose,
  applyMiddleware,
  combineReducers,
  Reducer,
  StoreEnhancer
} from 'redux';
import thunk, { ThunkMiddleware } from 'redux-thunk';
import { lazyReducerEnhancer } from 'pwa-helpers/lazy-reducer-enhancer.js';

import app, { AppState } from './reducers/app.js';
import { CounterState } from './reducers/counter.js';
import { ShopState } from './reducers/shop.js';
import { AppAction } from './actions/app.js';
import { CounterAction } from './actions/counter.js';
import { ShopAction } from './actions/shop.js';

import { saveState, loadState } from './localstorage.js';

// Overall state extends static states and partials lazy states.
export interface RootState {
  app?: AppState;
  counter?: CounterState;
  shop?: ShopState;
}

export type RootAction = AppAction | CounterAction | ShopAction;

// Sets up a Chrome extension for time travel debugging.
// See https://github.com/zalmoxisus/redux-devtools-extension for more information.
const devCompose: <Ext0, Ext1, StateExt0, StateExt1>(
  f1: StoreEnhancer<Ext0, StateExt0>, f2: StoreEnhancer<Ext1, StateExt1>
) => StoreEnhancer<Ext0 & Ext1, StateExt0 & StateExt1> =
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

// Initializes the Redux store with a lazyReducerEnhancer (so that you can
// lazily add reducers after the store has been created) and redux-thunk (so
// that you can dispatch async actions). See the "Redux and state management"
// section of the wiki for more details:
// https://github.com/Polymer/pwa-starter-kit/wiki/4.-Redux-and-state-management
export const store = createStore(
  state => state as Reducer<RootState, RootAction>,
  //(state, action) => state,
  loadState(),  // If there is local storage data, load it.
  devCompose(
    lazyReducerEnhancer(combineReducers),
    applyMiddleware(thunk as ThunkMiddleware<RootState, RootAction>))
);

// Initially loaded reducers.
store.addReducers({
  app
});

// This subscriber writes to local storage anytime the state updates.
store.subscribe(() => {
  saveState(store.getState());
});

The console log shows me that state is written and loaded. But inital data values of the components are back to initial values after reload - however, the localstorage is there with correct values?

Not sure what is missing here? Maybe something with the INITIAL_STATE?

const counter: Reducer<CounterState, RootAction> = (state = INITIAL_STATE, action) => { ... }

keanulee commented 5 years ago

Consider loading state from localstorage as an action, not when creating the store. In pwa-starter-kit-hn, there's a loadFavorites action creator that dispatches after indexedDB is ready (indexedDB loading being async necessitates this pattern, but it can also be used with localstorage).

luissardon commented 5 years ago

The saveState method is replacing the loaded state on refresh, in order to avoid that you have to persist the loaded state by something like:

export const saveState = state => {
    try {
        const loadedState = loadState() || {};
        const serializedState = JSON.stringify(mergeDeep(loadedState, state));
        localStorage.setItem('state', serializedState);
    } catch (err) {
        // Ignore write errors.
    }
};

and when you add a new reducer that you want to persist its loaded state, you have to do something like:

const loadedState = loadState() || {};
store.addReducers({
  app: (state, actions) => app(loadedState.app || state, actions)
});