davnicwil / react-frontload

Async data loading for React components, on client & server
451 stars 21 forks source link

Server Side Issues with Dispatch Action Method #38

Closed kandarppatel closed 5 years ago

kandarppatel commented 5 years ago

Hello,

Thank you for making such an amazing module to utilize SSR with front-end technologies.

We have been planning to use it for one of our project but having issues with overall consistent server side rendering. We have tried it using two types of redux actions, with direct return its works completely fine but with dispatch based return it gives weird results, sometimes getting data from SSR and sometimes not getting. Please find below reference of code to get more ideas about it.

Note : We have used basic setup from your blog.

Working Method 1 :

export function fetchCategories() {     
    const request = axios.get('${API_URL}/blog/categories');  
   return{  
        type: FETCH_CATEGORIES,  
        payload: request 
  };
}

Non Working Method 2 :

export function fetchCategories() {
  return function(dispatch) {
    return axios.get('${API_URL}/blog/categories')
    .then(response => {
      dispatch({
        type: FETCH_CATEGORIES,
        payload: response
      });
    })
    .catch((error) => {
      console.log(error);
    })
  }
}

We have try to research a lot but did not find any solution so hoping to get some help here.

davnicwil commented 5 years ago

Hi @kandarppatel -- you're welcome and really glad you're finding react-frontload useful.

I'd love to help out here if it's a specific problem you can identify with usage of react-frontload, but I'll need a complete example including how the above calls are used in a frontload Component and probably your SSR setup too, to be able to spot any potential issues.

Off the top of my head, though, I'll give it a shot - inconsistent SSR with react-frontload is usually the result of forgetting to return a Promise somewhere in a frontload. When that happens, the async logic in the frontload may sometimes resolve in time for final render by chance, and other times won't. Double check all your frontloads for this.

kandarppatel commented 5 years ago

Thank you for quick help, please use below details setup to understand in details.

server.js

import path from 'path';
import fs from 'fs';
import express from 'express';
import axios from 'axios';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router';
import { Provider } from 'react-redux';
import { Frontload, frontloadServerRender } from 'react-frontload';
import Loadable from 'react-loadable';
import Helmet from "react-helmet";

import routes from '../routes';
import reducers from '../reducers';
import createStore from '../store';

const app = new express();

app.use('/public', express.static(path.join(__dirname, '../../../public')))

app.use('/favicon.ico', (req, res) => {
  return;
});

const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackHotMiddleware = require('webpack-hot-middleware');
const config = require('../../../webpack.config');
const compiler = webpack(config);
app.use(webpackDevMiddleware(compiler, { serverSideRender: true, noInfo: true, publicPath: config.output.publicPath }));
app.use(webpackHotMiddleware(compiler));

const server = express();
app.use((req, res) => {
    const initialState = {};
    const { store } = createStore(req.url);

    const context = {};
    const modules = [];

    const initialComponent = (
        <Loadable.Capture report={moduleName => modules.push(moduleName)}>
          <Provider store={store}>
            <StaticRouter location={req.url} context={context}>
              <Frontload isServer={true}>
                { routes }
              </Frontload>
            </StaticRouter>
          </Provider>
        </Loadable.Capture>
    );

    frontloadServerRender(() =>
          renderToString(initialComponent)
    ).then(routeMarkup => {
          if (context.url) {
            res.writeHead(302, {
            Location: context.url
          });
          res.end();
        } else {
          const helmet = Helmet.renderStatic();
          const app_store = JSON.stringify(store.getState()).replace(/</g, '\\u003c');
          res.send(renderFullPage(routeMarkup, helmet, app_store));
        }
      });
});

function renderFullPage(html, head, initialState) {
  return `
    <!DOCTYPE html>
        <html ${head.htmlAttributes.toString()}>
            <head>
        ${head.title.toString()}
        ${head.meta.toString()}
        ${head.link.toString()}
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <link rel="stylesheet" href="/public/css/style.css">
        ${head.script.toString()}
            </head>
            <body>
                <div id="app">${html}</div>
                <script>window.__PRELOADED_STATE__ = ${initialState}</script>
        <script src="/public/vendor.js"></script>
        <script src="/public/client.js"></script>
            </body>
        </html>
    `
}

app.get('*', function(req, res) {
    res.status(404).send('Server.js > 404 - Page Not Found');
})

app.use((err, req, res, next) => {
  res.status(500).send("Server error");
});

process.on('uncaughtException', evt => {
  console.log( 'uncaughtException: ', evt );
})

const port = process.env.PORT || 7013;
const env = process.env.NODE_ENV || 'production';

Loadable.preloadAll().then(() => {
  app.listen(port, err => {
    if (err) {
      return console.error(err);
    }
    console.info(`Server running on http://localhost:${port} [${env}]`);
  });
});

store.js

import { createStore, applyMiddleware, compose } from 'redux';
import { connectRouter, routerMiddleware } from 'connected-react-router';
import thunk from 'redux-thunk';
import promise from 'redux-promise';
import { createBrowserHistory, createMemoryHistory } from 'history';
import reducers from './reducers';

// A nice helper to tell us if we're on the server
export const isServer = !(
  typeof window !== 'undefined' &&
  window.document &&
  window.document.createElement
);

export default (url = '/') => {
  // Create a history depending on the environment
  const history = isServer
    ? createMemoryHistory({
        initialEntries: [url]
      })
    : createBrowserHistory();

  const enhancers = [];

  // Dev tools are helpful
  if (process.env.NODE_ENV === 'development' && !isServer) {
    const devToolsExtension = window.devToolsExtension;

    if (typeof devToolsExtension === 'function') {
      enhancers.push(devToolsExtension());
    }
  }

  const middleware = [promise, thunk, routerMiddleware(history)];
  const composedEnhancers = compose(
    applyMiddleware(...middleware),
    ...enhancers
  );

  // Do we have preloaded state available? Great, save it.
  const initialState = !isServer ? window.__PRELOADED_STATE__ : {};

  // Delete it once we have it stored in a variable
  if (!isServer) {
    delete window.__PRELOADED_STATE__;
  }

  // Create the store
  const store = createStore(
    connectRouter(history)(reducers(history)),
    initialState,
    composedEnhancers
  );

  return {
    store,
    history
  };
};

reducers.js

import { combineReducers } from 'redux';
import AppReducer from '../container/app/app_reducer';
import CatalogReducer from '../container/catalog/catalog_reducer';
import ArticleReducer from '../container/article/article_reducer';
import { reducer as formReducer } from 'redux-form';
import { reducer as modalReducer } from 'react-redux-modal';
import { connectRouter } from 'connected-react-router';

const rootReducer  = (history) =>  combineReducers({
  router: connectRouter(history),
  form: formReducer,
  modals: modalReducer,
  app: AppReducer,
  catalog: CatalogReducer,
  article: ArticleReducer
});

export default rootReducer;

index.js (Client Side - Reference)

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { ConnectedRouter, routerMiddleware } from 'connected-react-router';
import { Frontload } from 'react-frontload';

import routes from '../routes';
import createStore from '../store';

const { store, history } = createStore();

ReactDOM.render(
  <Provider store={store}>
    <ConnectedRouter history={history}>
      <Frontload noServerRender={true}>
        { routes }
      </Frontload>
    </ConnectedRouter>
  </Provider>
  , document.querySelector('#app'));

business_list.js (Container Which Calls above actions)

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { frontloadConnect } from 'react-frontload';
import { fetchCategories } from './catalog_action';
import { Link } from 'react-router-dom';
import Helmet from "react-helmet";
import { IMAGE_URL, DEFAULT_IMAGE } from '../../system/config';
import CardSmall from './card_small';

const loadCategories = async props =>
  await props.fetchCategories();

class CatalogCategory extends Component {

  constructor(props) {
      super(props);
  }

  componentDidMount() {
    this.props.fetchCategories();
  }

  renderCategories() {
    return this.props.categories.map((category) => {
        return (
          <div className="col-sm-3 col-xs-6" key={category.category_id}>
            <CardSmall article={category} type="business" />
          </div>
        );
    });
  }

  render() {

    const { categories } = this.props;

    if(!categories) {
        return <div className="home-container">Loading...</div>;
    }

  return (
      <div className="">
        <Helmet>
          <title>Business Categories & Resources</title>
          <meta name="description" content="Business Tools & Resources" />
        </Helmet>
      <div className="home-container">
          <div className="home-box">
            <h1 className="home-title">Categories</h1>
            <div className="home-sub-title">Business Categories & Resources</div>
          </div>
        </div>
        <div className="container">
          <div className="row popular-articles">
            {this.renderCategories()}
          </div>
        </div>
      </div>
    );
  }
}

function mapStateToProps(state) {
  return {
    categories: state.catalog.categories
  };
}

export default connect(mapStateToProps, { fetchCategories })(
  frontloadConnect(loadCategories, {
    onMount: true,
    onUpdate: false
  })(CatalogCategory)
);

webpack.config.js

module.exports = {
  mode: 'production',
  entry: {
    client: './src/system/client/index.js',
    vendor: ['react', 'react-dom', 'react-router-dom'],
  },
  output: {
    path: __dirname,
    publicPath: '/',
    filename: 'public/[name].js'
  },
  module: {
    rules: [
      { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" }
    ]
  },
  resolve: {
    extensions: ['*', '.js', '.jsx']
  },
  devServer: {
    port: 7013,
    historyApiFallback: true,
    contentBase: './'
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        commons: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          chunks: 'all'
          }
        }
    },
  }
};

.babelrc

{
  presets:
    [
        "@babel/preset-env",
        "@babel/preset-react"
    ],
    plugins:
    [
        "@babel/plugin-proposal-object-rest-spread",
        "@babel/plugin-transform-runtime",
        "@babel/plugin-transform-async-to-generator",
        "@babel/plugin-proposal-class-properties"
    ]
}

I hope all above will help you to understand our setup and specific issue. Let me know if you need anything else.

Once again thank you for your help.

davnicwil commented 5 years ago

Great - so what's a specific scenario of the bug you're seeing?

Which route is rendered, and then what is rendered in the success case and failure case?

kandarppatel commented 5 years ago

Issue is for Server Side Rendering, if we use the action format 1 mentioned than it all works completely fine but when we use action format 2 with dispatch method than its not working consistently, sometimes waiting for promise and getting the perfect data and sometimes missing the data so not sure why its not working consistently and getting data all the time.

We have tested this in Browser View Page Source using refresh, First time we load its coming, than reload than yes coming than 3rd time not, than 4th time not, than 5th time yes so its randomly waiting for promise results or its not at all checking all the promises from above dispatch based actions format, if data getting faster from api than its passing the data instantly and if api takes little longer than its not passing at all .

Using action format 1 mentioned below (using direct return without dispatch), it all works perfect and consistent.

Action Format 1 (Working Fine) :

export function fetchCategories() {     
    const request = axios.get('${API_URL}/blog/categories');  
   return{  
        type: FETCH_CATEGORIES,  
        payload: request 
  };
}

Action Format 2 (Working Randomly, Not Consistent) :

export function fetchCategories() {
  return function(dispatch) {
    return axios.get('${API_URL}/blog/categories')
    .then(response => {
      dispatch({
        type: FETCH_CATEGORIES,
        payload: response
      });
    })
    .catch((error) => {
      console.log(error);
    })
  }
}

I hope now you will clearly understand the issue.

davnicwil commented 5 years ago

I think this is because you're not returning the result of the dispatch in the second method, which is a promise with redux-thunk.

This would mean that the action creator isn't returning the promise of the request, meaning the frontload also isn't returning the promise, meaning react-frontload doesn't wait for the request to finish on server renders.

This would explain the random behaviour you're seeing with it sometimes loading in time by chance, if the api is quick, and sometimes not if it's a bit slower.

Give this a try, and let me know if it fixes it, and if that all makes sense:

.then(response => (
  dispatch({
    type: FETCH_CATEGORIES,
    payload: response
  })
))

(note I just turned the {} braces into () for immediate return)

kandarppatel commented 5 years ago

Sorry for replying little late but have tried it and its giving syntax error.

Will do more research and update you once find any solution.

davnicwil commented 5 years ago

Hm, you shouldn't be getting any syntax error from just returning at that point. What is the error?

Did the explanation about the Promises make sense? I don't think you need to do any more research, I think that's the problem :-)

davnicwil commented 5 years ago

Hey @kandarppatel, any joy with what I suggested?

kandarppatel commented 5 years ago

Yes finally it worked out, not sure what was the exact issue but may be in thunk promise middle-ware but its all working fine now. Thank you so much for your support.

matthewlein commented 2 years ago

I had the same problem, it turns out I was not returning my dispatched actions. @davnicwil Thanks for the tip.

For an example, this is now a working thunk

export function fetchUser(): RequestThunk {
  return (dispatch) => {
    dispatch(UserActions.USER_FETCH_PENDING());

    return axios
      .get('users/me')
      .then((response) => {
        return dispatch(UserActions.USER_FETCH_SUCCESS(response.data));
      })
      .catch((error) => {
        return dispatch(UserActions.USER_FETCH_ERROR(error));
      });
  };
}

How it it worked in v1...I'll never know