Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using redux with redux-persist with server-side rendering

I am trying to implement redux 4.0.0 with redux-persist 5.10.0 in an SSR application and am running into an issue where I cannot properly supply createStore() with the preloaded state without the app crashing.

What happens is that the application loads with the initial state from the server, but when the app tries to preload the state in createStore() on the client, the app refreshes and crashes. I assume it's because my preloadedState is not in the proper format?

But I'm not sure because I'm not getting any error messages in the console, the UI, nada.

Here is some relevant code:

store/index.js

export default function configureStore(preloadedState = {}) {
    // This will store our enhancers for the store
    const enhancers = [];

    // Add thunk middleware
    const middleware = [thunk];

    // Apply middlware and enhancers
    const composedEnhancers = compose(
        applyMiddleware(...middleware),
        ...enhancers
    );

    // Set up persisted and combined reducers
    const persistedReducer = persistReducer(persistConfig, rootReducer);

    // Create the store with the persisted reducers and middleware/enhancers
    const store = createStore(persistedReducer, preloadedState, composedEnhancers);

    const persistor = persistStore(store, null, () => {
        store.getState(); // if you want to get restoredState
    });

    return { store, persistor };
}

index.js

const preloadedState = window.__PRELOADED_STATE__ ? window.__PRELOADED_STATE__ : {};
delete window.__PRELOADED_STATE__;

// Create redux store
const { persistor, store } = configureStore(preloadedState);

// Get app's root element
const rootEl = document.getElementById("root");

// Determine if we should use hot module rendering or DOM hydration
const renderMethod = !!module.hot ? ReactDOM.render : ReactDOM.hydrate;

renderMethod(
    <Provider store={store}>
        <PersistGate loading={<Loader />} persistor={persistor}>
            <BrowserRouter>
                <App />
            </BrowserRouter>
        </PersistGate>
    </Provider>,
    rootEl
);

Things persist and whatnot in development on the client, but when I test the SSR the app loads, and then reloads and goes blank. It reloading has me thinking the state is not being hydrated with the same data. it crashing completely has me baffled at the moment.

Any idea of how to proceed??

EDIT

After some old-school debugging, I've found that removing the <PersistGate loading={<Loader />} persistor={persistor}> line will allow the app to load initially and things are loaded via the server as expected, but the data does not persist properly (obviously).

Is there anything wrong with how I'm using the PersistGate component?

window.__PRELOADED_STATE__

{
    user: {…}, banners: {…}, content: {…}, locations: {…}, news: {…}, …}
    banners: {isLoading: 0, banners: Array(2)}
    content: {isLoading: 0, errors: {…}, data: {…}}
    locations: {countries: Array(0), provinces: Array(0), default_country: null, isLoading: false, error: null, …}
    news: {isLoading: 0, hasError: 0}
    phoneTypes: {isLoading: false}
    profileStatuses: {isLoading: false}
    profileTypes: {isLoading: false}
    reviewers: {isLoading: false}
    route: {}
    salutations: {isLoading: false}
    sectors: {isLoading: false, sectors: Array(0)}
    siteInfo: {pageTitle: "", isLoading: 0, hasError: 0, error: "", site: {…}, …}
    sort: {value: "", dir: ""}
    user: {isLoading: false, loginChecked: {…}, admin: null, reviewer: null, loginTokenLoading: false, …}
    _persist: {version: -1, rehydrated: true}
    __proto__: Object
}
like image 371
Gurnzbot Avatar asked Nov 06 '18 14:11

Gurnzbot


3 Answers

I have this working with the following setup within NextJs.

When window isn't defined (on the server), I render the app without the PersistGate.

The store configuration is required to not take storage, which I determine based on the passed prop.

class MyApp extends App {
  public render() {
    const { Component, pageProps } = this.props;
    if (typeof window === "undefined") {
      const { store } = configureStore();
      return (
        <Provider store={store}>
          <Component {...pageProps} />
        </Provider>
      );
    }
    const { store, persistor } = configureStore(storage);

    return (
      <Provider store={store}>
        <PersistGate loading={null} persistor={persistor}>
          <Component {...pageProps} />
        </PersistGate>
      </Provider>
    );
  }
}

export default MyApp;
const configureStore = (passedStorage?: AsyncStorage | WebStorage) => {
  const combinedReducers = combineReducers({
    conjugations: conjugationReducer
  });
  if (!passedStorage) {
    const store = createStore(combinedReducers);
    return { store };
  }

  const persistConfig = {
    key: "root",
    storage: passedStorage
  };
  const persistedReducer = persistReducer(persistConfig, combinedReducers);

  const store = createStore(
    persistedReducer
  );
  const persistor = persistStore(store);
  return { store, persistor };
};
like image 167
Powderham Avatar answered Nov 08 '22 22:11

Powderham


When you use Redux-persist with SSR, It creates crashes, problems like It shows you white screen for 1-5 seconds and then shows the page.

It's a problem of Persist + Hydrate, To solve it, try below solution. :)

  1. Remove Redux-persist. Lol just kidding!
  2. Remove <PersistGate> and use the code like below

Code

function Main() {
   return (
       <Provider store={store}>
         // Don't use <PersistGate> here.
         <Router history={history}>
            { Your other code }
         </Router>
       </Provider>
   );
}

persistor.subscribe(() => {
   /* Hydrate React components when persistor has synced with redux store */
   const { bootstrapped } = persistor.getState();

   if (bootstrapped) {
      ReactDOM.hydrate(<Main />, document.getElementById("root"));
   }
});
like image 42
Krunal Panchal Avatar answered Nov 08 '22 21:11

Krunal Panchal


One solution which worked for me is the following -

  • Have all the actions and reducers defined in a store - without any use of redux-persist. Expose the createStore method which takes reducer as an argument.
  • On the server, import the reducers defined in the store and create the store, renderToString().
  • On the client, import the same reducers, create a persisted reducer using 'storage' (note that 'storage' doesn't work on server so we've to import it only in client). Also, create the store using the redux state sent from server, and this persisted reducer. Now persist this store and use this store (in Provider) and persistor (in PersistGate)

For me, it works well if all the variables that I'm trying to persist are part of components. Other variables you can manage using post calls to the server (using {axios} within the components).

Check this repo for creating store without redux-persist - follow above steps after that - https://github.com/alexnm/react-ssr/tree/fetch-data

// in client.js
import {createStore as createPersistedStore} from 'redux';
import createStore, { reducers } from './store';
const persistConfig = {
  key: 'app',
  storage,
}

const persistedReducers = persistReducer(persistConfig, reducer);
// creating a persisting store in client.js only
const store = createStore(persistedReducers, window.REDUX_DATA);
const persistor = persistStore(store);

const jsx = (
  <ReduxProvider store={store}>
    <PersistGate loading={"loading from store"} persistor={persistor}>
      <Router>
        <App />
      </Router>
    </PersistGate>
  </ReduxProvider>
);

const app = document.getElementById("app");
ReactDOM.hydrate(jsx, app);
// end client.js


// in server.js - only see createStore as well as const jsx object, and a dummy context
import createStore, { reducers } from './store';
const app = express()
app.get( "/*", (req, res, next) => {
  const context = {};
  // not created using persisted store in the server - don't have to
  const store = createStore(reducer);
  // define data with the routes you need (see the github repo)
  Promise.all(data).then(() => {
    const jsx = (
        <ReduxProvider store={store}>
            <StaticRouter context={context} location={req.url}>
                <App />
            </StaticRouter>
        </ReduxProvider>
    );
    const reactDom = renderToString(jsx);
    const reduxState = store.getState();
    // more code for res.end
  });
});
// end server.js

// in store.js
import {createStore, combineReducers, applyMiddleware} from "redux";
// your actions and reducers
export const reducer = combineReducers({
  // reducers
)};
export default (reducerArg, initialState) =>
  createStore(reducerArg,initialState);
// end store.js
like image 2
noviceprogrammer Avatar answered Nov 08 '22 21:11

noviceprogrammer