microsoft / redux-dynamic-modules

Modularize Redux by dynamically loading reducers and middlewares.
https://redux-dynamic-modules.js.org
MIT License
1.07k stars 115 forks source link

Usage with redux-persist #94

Open dbartholomae opened 5 years ago

dbartholomae commented 5 years ago

Hi there!

I really like this library - and I also really like redux-persist which allows to save parts of the store to local storage. Unfortunately both this module and redux-persist require to use their own method to create a redux store, and these methods are not chainable if I am not mistaken. What would the preferred way to solve this? If there is no way yet but an idea of how to solve this as an additional feature in this library, I would be willing to try myself on a PR. I have also opened an issue with the other repo.

Best, Daniel

abettadapur commented 5 years ago

We could look and see how redux-persist works and perhaps create a new extension for it

abettadapur commented 5 years ago

I can investigate this more this week

dbartholomae commented 5 years ago

Great! If there is any way I can help, please let me know. In the mean time I will be working on a plugin for redux beacon

MegazeroMZ commented 5 years ago

Hey there, I happened to do this exact thing recently (using redux persist along side redux dynamic module) and here's how I did it

have a normal config for your persist:

import { persistReducer } from "redux-persist";

const persistConfig = {key: "PersistKey", storage, whitelist, blacklist};
const persistedReducer = persistReducer(config, yourReducer)

And then create a module for your use with redux dynamic module:

export function getYourModule() {
  id: "YourID",
  reducerMap: {
    reducer: persistedReducer
  }
}

Now the main problem with redux dynamic module is that your module isnt loaded until it being render, so with all we did so far at the start of your app the persistedReducer didnt get put into store yet, so you need to call .persist() on the constructor of your component (which got called whenever your component got render and getYourModule get put into the redux dynamic module's store) to tell redux persistor that you have just loaded this new reducer, so now it should try to look for the persist data of the newly loaded reducer.

As for the store, you can do something like this:

import { createStore } from 'redux-dynamic-modules';
import { persistStore } from 'redux-persist';

const store = createStore(.......);
const persistStore = persistStore(store);

Save this persistStore in some way, so on the constructor of your component like I talk about earlier you can call "persistStore.persist();", and it should persist your reducer in that dynamic module now

MegazeroMZ commented 5 years ago

@MegazeroMZ can you give a better example on how/when/where you are calling persistStore.persist(); ?

So continue with what I already have in the previous post, let's say I have a component called Setting, and I making it a dynamic module using redux-dynamic-module, then I will have this:

<DynamicModuleLoader modules={getYourModule()}>
  <Setting />
</DynamicModuleLoader>

Now once this Setting got render on the screen, getYourModule() will finally be put into the store by redux-dynamic-module, so now if you log out the state of your reducer you will finally see the state in the persistedReducer got put into the store. So now is when you can call persistStore.persist(), so it make sense to call it in the constructor of Setting component, so you will have Setting component look something like this:

// import persistStore from where you saved it
class Setting extends React.Component {
    constructor(props) {
        super(props);
        persistStore.persist();
    }
   ... // the rest of the component
subtirelumihail commented 5 years ago

@MegazeroMZ thanks, I already figured it out and it works.

Also it is worth mentioning that the PersistGate component is not working with redux-dynamic-modules. When I tried using it it get's stuck at the loading phase

MegazeroMZ commented 5 years ago

@MegazeroMZ thanks, I already figured it out and it works.

Also it is worth mentioning that the PersistGate component is not working with redux-dynamic-modules. When I tried using it it get's stuck at the loading phase

PersistGate does work with redux-dynamic-modules.

This one is slightly more complicated. The role of the PersistGate is to stop the app from being load until the persist has check all the reducers for persisted stuff. BUT, and a big BUT here, if there's no persistedReducer passed to the persistor prop in PersistGate, then PersistGate thinking that it havent check all reducer yet cause it couldnt find any persistedReducer, hence it stuck in loading, or stuck in PersistGate in this case. In case the only persistedReducer you use are inside 1 of the redux-dynamic-module, because it havent been load yet so PersistGate cant find it, hence it stuck.

There's 2 way around this. 1 is you cheated by purposely add a persistedReducer of some unimportant, even unused, reducer to the persistor prop in PersistGate. 2 is you check if there's any persistedReducer in the persistorStore or not, if not then dont use PersistGate, if there is then use PersistGate. You can check this by checking persistStore.getState().registry.length, if it > 0 then you have some persistedReducer in the store, if its 0 then you dont.

danmarvia commented 4 years ago

Would it be possible to add this implementation to one of the examples here https://github.com/microsoft/redux-dynamic-modules/tree/master/packages?

dbartholomae commented 4 years ago

This is how it worked for us:

const authModule = {
  id: 'auth',
  reducerMap: {
    auth: persistReducer(createPersistConfig('local_storage_auth'), authReducer)
  }
}

export const store = createStore({}, authModule)
export const persistor = persistStore(store)

This way each module can decide on its own what to persist, but localStorage will be a bit cluttered

leonardofalk commented 4 years ago

Just use advancedCombineReducers:

// createStore.ts
import { combineReducers } from 'redux'
import { createStore as createDynamicStore } from 'redux-dynamic-modules'
import { createReducer } from 'reduxsauce' // can use your own reducers as well
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage'

const reducer = createReducer({}, { _: state => state }) // or write your own initial reducer

const mandatoryInitialModule = {
  id: '_initial_module',
  reducerMap: { _: reducer }
}

export const createStore = () => {
  const store = createDynamicStore(
      {
        extensions: [...],
        advancedCombineReducers: reducers => // combineReducers by default, see https://github.com/microsoft/redux-dynamic-modules/blob/7492972e1d4f969eae70c28ec9fc55f7cbb75e55/packages/redux-dynamic-modules-core/src/Managers/ReducerManager.ts#L52
          persistReducer(
            {
              key: '2019_12_16',
              storage,
              whitelist: ['user_session'], // or whatever reducer may appear later
            },
            combineReducers(reducers as any)
          ) as any,
      },
      mandatoryInitialModule
    )

  const persistor = persistStore(store)

  return { store, persistor }
}

then:

// App.tsx
import React from 'react'
import { Provider } from 'react-redux'
import { PersistGate } from 'redux-persist'
import { createStore } from './createStore'

const { store, persistor } = createStore()

const App = ({ children }: any) => (
  <Provider store={store}><PersistGate persistor={persistor}>{children}</PersistGate></Provider>
)

export default App

As mentioned by @MegazeroMZ, you need that initial mandatory reducer to fake PersistGate default behaviour, otherwise you'll se an infinite loading screen. As for creating an extension for it, dynamic-modules should expose an API for the store, because right after inserting and extension we must call persistStore and save the persistor somewhere.

ShubhamJoshi-1994 commented 4 years ago

This is how it worked for us:

const authModule = {
  id: 'auth',
  reducerMap: {
    auth: persistReducer(createPersistConfig('local_storage_auth'), authReducer)
  }
}

export const store = createStore({}, authModule)
export const persistor = persistStore(store)

This way each module can decide on its own what to persist, but localStorage will be a bit cluttered

@dbartholomae does this means that we export a store from each of the modules? Is there any example implementation using the functional components?

dbartholomae commented 4 years ago

No - the first part is in each module, the second part only in the central store creation. Unfortunately I don't have a public example at the moment.

ShubhamJoshi-1994 commented 4 years ago

No - the first part is in each module, the second part only in the central store creation. Unfortunately I don't have a public example at the moment.

Ok no worries, I tried it as you have said and it is working. Thanks.

olabala commented 3 years ago

any update? I also encountered the same problem. After dynamically registering the module, I can no longer write to the local cache.

olabala commented 3 years ago

found a solution

import { persistCombineReducers } from 'redux-persist'

export const advancedCombineReducers = (reducers) => persistCombineReducers(PersistConfig, reducers)

and register a redux-saga watcher: call persist when 'moduleAdd' and 'moduleRemove' action called

function * watchDynamic () {
  yield takeEvery(['@@Internal/ModuleManager/ModuleAdded', '@@Internal/ModuleManager/ModuleRemoved'], function * () {
    persistor.persist()
  })
}
juan-salazar-jobnimbus commented 2 years ago

Here is another solution, based on the responses given by @olabala and @leonardofalk:

  1. Go where you are creating the dynamic store root configuration:
import { createStore as createDynamicStore } from "redux-dynamic-modules";

...

export const store = createDynamicStore(
  {
    ...,
    advancedCombineReducers: (reducers) =>
      persistCombineReducers(
        {
          key: "root",
          storage,
          whitelist: [],
        },
        reducers
      ),
  },
  initialModuleWithReducer() // <= this is mandatory!
);

export const persistor = persistStore(store);
  1. Go to your root component where you are using <Provider> (generally App), and use the persistor variable created previously:
<Provider store={store}>
        <PersistGate persistor={persistor}>
        {children}
        </PersistGate>
</Provider>
  1. In the module you want to lazy load, add the following inside the dynamic module configuration:
export function moduleConfig() {
  return {
    id: "the-module-slice",
    reducerMap: {
      "the-slice": persistReducer(
        {
          key: "the-slice",
          storage,
          whitelist: ["draft"] // if you want to save a specific piece of your slice. In this case, draft belongs to "the-slice" part of the store
        },
        rootReducer
      ),
    },
    ...
  };
}
  1. In the module you want to lazy load Create a new saga listener, to the actions that redux-dynamic-modules triggers when a module is added/removed. this way, it can call the persist action explicitly:
export function* rootSaga() {
  yield all([watchDynamicReduxModules()/* ... other sagas.... */]);
}

function* watchDynamicReduxModules() {
  yield takeEvery(
    [
      "@@Internal/ModuleManager/ModuleAdded",
      "@@Internal/ModuleManager/ModuleRemoved",
    ],
    function* () {
      persistor.persist();
    }
  );
}

Leave me a reaction/comment to know if the same strategy works for you, so maybe it could be a workaround while the official plugin is released.

ivp-dev commented 2 years ago

Hi

If someone needs this feature I have created a repo to make your dynamic modules persistable.