cereallarceny / cra-ssr

[DEPRECATED] Server-side rendering with create-react-app, React Router v4, Helmet, Redux, and Thunk
483 stars 120 forks source link

SSR not working #52

Closed marinav closed 5 years ago

marinav commented 5 years ago

Hi,

First thank you for your awesome work. I'm using your repo since last july. I took it because of SSR, it was working fine but now I found that SSR is not working anymore. When I curl my page, I see all html exept my async data, I see the window.__PRELOADED_STATE__ but the store is empty. I tryied to update my version like you did but it's not working better. You can see my code below, may be you will see what I missed...thank you very much !

This is my package.json dependencies :

  "dependencies": {
    "@mars/heroku-js-runtime-env": "^3.0.2",
    "@sentry/browser": "^4.4.1",
    "autoprefixer": "^7.1.6",
    "babel-core": "^6.26.0",
    "babel-eslint": "^7.2.3",
    "babel-jest": "^20.0.3",
    "babel-loader": "^7.1.2",
    "babel-polyfill": "^6.26.0",
    "babel-preset-react-app": "^3.1.1",
    "babel-runtime": "^6.26.0",
    "basic-auth": "^2.0.0",
    "bodymovin": "^4.13.0",
    "bootstrap": "^4.1.1",
    "case-sensitive-paths-webpack-plugin": "^2.1.1",
    "chalk": "^1.1.3",
    "classnames": "^2.2.6",
    "connected-react-router": "^4.3.0",
    "cookie-parser": "^1.4.3",
    "css-loader": "^0.28.7",
    "dotenv": "^4.0.0",
    "dotenv-expand": "^4.2.0",
    "eslint": "^4.10.0",
    "eslint-config-react-app": "^2.1.0",
    "eslint-loader": "^1.9.0",
    "eslint-plugin-flowtype": "^2.39.1",
    "eslint-plugin-import": "^2.8.0",
    "eslint-plugin-jsx-a11y": "^5.1.1",
    "eslint-plugin-react": "^7.4.0",
    "extract-text-webpack-plugin": "^3.0.2",
    "file-loader": "^1.1.5",
    "file-saver": "^2.0.0",
    "font-awesome": "^4.7.0",
    "forcedomain": "^1.0.0",
    "fs-extra": "^3.0.1",
    "html-webpack-plugin": "^2.29.0",
    "http-proxy-middleware": "^0.19.1",
    "ignore-styles": "^5.0.1",
    "isomorphic-fetch": "^2.2.1",
    "jest": "^20.0.4",
    "jquery": "^3.3.1",
    "js-cookie": "^2.2.0",
    "jsdom": "^12.0.0",
    "libphonenumber-js": "^1.6.9",
    "lodash": "^4.17.10",
    "md5-file": "^4.0.0",
    "moment": "^2.22.2",
    "morgan": "^1.9.0",
    "node-sass-chokidar": "^1.3.0",
    "normalize.css": "^8.0.0",
    "npm-run-all": "^4.1.3",
    "object-assign": "^4.1.1",
    "postcss-flexbugs-fixes": "^3.2.0",
    "postcss-loader": "^2.0.8",
    "prettier": "^1.13.4",
    "promise": "^8.0.1",
    "raf": "^3.4.0",
    "react": "^16.4.0",
    "react-app-polyfill": "^0.2.0",
    "react-autosuggest": "^9.4.2",
    "react-circular-progressbar": "^1.0.0",
    "react-dev-utils": "^5.0.1",
    "react-device-detect": "^1.6.1",
    "react-dom": "^16.4.0",
    "react-frontload": "^1.0.1",
    "react-gtm-module": "^2.0.2",
    "react-helmet": "^5.2.0",
    "react-html-parser": "^2.0.2",
    "react-intercom": "^1.0.14",
    "react-loadable": "^5.4.0",
    "react-lottie": "^1.2.3",
    "react-redux": "^5.0.7",
    "react-reveal": "^1.2.2",
    "react-router-dom": "^4.2.2",
    "react-select": "^1.2.1",
    "react-share": "^2.2.0",
    "react-stripe-elements": "^2.0.2",
    "react-styleguidist": "^7.0.17",
    "react-table": "^6.8.6",
    "react-tabs": "^2.3.0",
    "react-typing-animation": "^1.4.0",
    "recharts": "^1.2.0",
    "redux": "^4.0.0",
    "redux-logger": "^3.0.6",
    "redux-segment": "^1.6.2",
    "redux-thunk": "^2.3.0",
    "resolve": "^1.6.0",
    "style-loader": "^0.19.0",
    "sw-precache-webpack-plugin": "^0.11.4",
    "url-loader": "^0.6.2",
    "webpack": "^3.8.1",
    "webpack-dev-server": "^2.9.4",
    "webpack-manifest-plugin": "^1.3.2",
    "whatwg-fetch": "^2.0.3",
    "xml2js": "^0.4.19"
  }

This is my component :

import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { frontloadConnect } from 'react-frontload';

const frontload = async props => {
  await props.getData();
}
class Test extends React.Component {

  componentDidMount() {
    this.props.getData()
  }

  render() {
    return (
      <Page
        id='test'
        title="test"
      >
       <h1>test</h1>
      </Page>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    test: state.test
  }
};

const mapDispatchToProps = dispatch =>
  bindActionCreators({ getData }, dispatch);

export default connect(mapStateToProps, mapDispatchToProps)(
  frontloadConnect(frontload, {
    onMount: true,
    onUpdate: false
  })(Test)
);

This is the frontload section in my loader.js file

      /*
        Here's the core funtionality of this file. We do the following in specific order (inside-out):
          1. Load the <App /> component
          2. Inside of the Frontload HOC
          3. Inside of a Redux <StaticRouter /> (since we're on the server), given a location and context to write to
          4. Inside of the store provider
          5. Inside of the React Loadable HOC to make sure we have the right scripts depending on page
          6. Render all of this sexiness
          7. Make sure that when rendering Frontload knows to get all the appropriate preloaded requests

        In English, we basically need to know what page we're dealing with, and then load all the appropriate scripts and
        data for that page. We take all that information and compute the appropriate state to send to the user. This is
        then loaded into the correct components and sent as a Promise to be handled below.
      */
      frontloadServerRender(() =>
        renderToString(
          <Loadable.Capture report={m => modules.push(m)}>
            <Provider store={store}>
              <StaticRouter location={req.url} context={context}>
                <Frontload isServer>
                  <App />
                </Frontload>
              </StaticRouter>
            </Provider>
          </Loadable.Capture>
        )
      ).then(routeMarkup => {
        if (context.url) {
          // If context has a url property, then we need to handle a redirection in Redux Router
          res.writeHead(302, {
            Location: context.url
          });

          res.end();
        } else {
          // Otherwise, we carry on...

          // Let's give ourself a function to load all our page-specific JS assets for code splitting
          const extractAssets = (assets, chunks) =>
            Object.keys(assets)
              .filter(asset => chunks.indexOf(asset.replace('.js', '')) > -1)
              .map(k => assets[k]);

          // Let's format those assets into pretty <script> tags
          const extraChunks = extractAssets(manifest, modules).map(
            c => `<script type="text/javascript" src="/${c}"></script>`
          );

          // We need to tell Helmet to compute the right meta tags, title, and such
          const helmet = Helmet.renderStatic();

          // NOTE: Disable if you desire
          // Let's output the title, just to see SSR is working as intended
          console.log('THE TITLE', helmet.title.toString());

          // Pass all this nonsense into our HTML formatting function above
          const html = injectHTML(htmlData, {
            html: helmet.htmlAttributes.toString(),
            title: helmet.title.toString(),
            meta: helmet.meta.toString(),
            body: routeMarkup,
            scripts: extraChunks,
            state: JSON.stringify(store.getState()).replace(/</g, '\\u003c')
          });

          // We have all the final HTML, let's send it to the user already!
          res.status(context.status || 200).send(html)
        }
      });

And my index.js file :

import React from 'react';
import { render, hydrate } from 'react-dom';
import { Provider } from 'react-redux';
import Loadable from 'react-loadable';
import { Frontload } from 'react-frontload';
import { ConnectedRouter } from 'connected-react-router';
import createStore from './store';

import App from './app/app';

// Create a store and get back itself and its history object
const { store, history } = createStore();

// Running locally, we should run on a <ConnectedRouter /> rather than on a <StaticRouter /> like on the server
// Let's also let React Frontload explicitly know we're not rendering on the server here
const Application = (
  <Provider store={store}>
    <ConnectedRouter history={history}>
      <Frontload noServerRender>
        <App />
      </Frontload>
    </ConnectedRouter>
  </Provider>
);

const root = document.querySelector('#root');

if (process.env.NODE_ENV !== 'development') {
  // If we're running in production or staging, we use hydrate to get fast page loads by just
  // attaching event listeners after the initial render
  Loadable.preloadReady().then(() => {
    hydrate(Application, root);
  });
} else {
  // If we're not running on the server, just render like normal
  render(Application, root);
}
francisrod01 commented 5 years ago

Hi @marinav what's up? Please could you reproduce the problem in codesandbox?

CodeSandbox is an online editor that's built for web application development. Web application development is a growing field, and with all new configuration options it becomes harder and harder to focus on writing code.

Regards!

czco commented 5 years ago

@marinav can you put ur example in a git repo. I'd love to just clone it and take a look

marinav commented 5 years ago

Hi everyone, thank you for your answers. I will try to put an example when I have a little time because it is a private project I can not share it as simply.

marinav commented 5 years ago

Hi,

I restarted from scratch, I clone this repo, and instead of simulate an API call, I updated getCurrentProfile function (in /modules/profile.js) to call my own api (like proposed here #33).

I can see my data, but when I curl my page after a yarn build && yarn serve I don't see them, and currentProfile is empty. But when I try with the original function it works. This is the only difference I've done in the project :

The original function (with working SSR)

export const getCurrentProfile = id => dispatch =>
  new Promise(resolve => {
    setTimeout(() => {
      let profile;

      if (id === 1) {
        profile = {
          id,
          name: 'Pekka Rinne',
          image: pekka
        };
      } else {
        profile = {
          id,
          name: 'Viktor Arvidsson',
          image: arvidsson
        };
      }

      dispatch({
        type: SET_CURRENT_PROFILE,
        profile
      });

      resolve(profile);
    }, 3000);
  });

The new function (with non working SSR)

export const getCurrentProfile = id => dispatch =>
  fetch('https://myapi/data', {
    headers: {
      Authorization: `Bearer xxxxxxxxxxx`,
      'Content-Type': 'application/x-www-form-urlencoded'
    }
  })
    .then(res => res.json())
    .then(
      result => {
        dispatch({
          type: SET_CURRENT_PROFILE,
          profile: {
            id,
            name: result.name,
            image: arvidsson
          }
        });
      },
      error => {
        console.log(error.message);
      }
    )

What am I doing wrong ??

Thank you !

francisrod01 commented 5 years ago

@marinav Please show us the screenshot of the Google Chrome console log. That's how we can start to figure out.

sabitertan commented 5 years ago

If you rely on json structure in my example API, you should use something like below

 export const getCurrentProfile = id => dispatch =>
  fetch("https://myapi/data/profile/" + id, {
    headers: {
      "Authorization": "Bearer xxxxxxxxxxx",
      "Content-Type": "application/x-www-form-urlencoded"
    }
  })
    .then(res => res.json())
    .then(
      result => {
        dispatch({
          type: SET_CURRENT_PROFILE,
          profile: {
            id: id,
            name: result.profile.name,
            image: result.profile.image
          }
        });
      },
      error => {
        console.log(error.message);
      }
    );
marinav commented 5 years ago

@sabitertan in fact I call an external API that doesn't return a profile user but a list of producers, so I make the call, and put in profile's name the name of my first producer. I'm just testing so the profile id doesn't really matter.

@francisrod01 I have no errors in my google chrome console log, but here a screenshot of my result and network request (the name is the name I get from my api but on curl nothing...) :

Capture d’écran 2019-03-21 à 15 40 43

sabitertan commented 5 years ago

Ah ok "curl" only issue, I will test later and check the issue.

francisrod01 commented 5 years ago

@marinav If it's just a test, why are you hiding the urls in the screenshot? I'm confuse about your way to fetch the data. I think you should change the content-type to application/json and perform new tests.

Reference:

marinav commented 5 years ago

Hi guys thank you for your answers !

@francisrod01 in fact it's a private project and I don't necessarily want to publish the staging url of my api. But it could be any url actually I don't feel that the problem comes from there. I still changed the content-type but it's the same thing 😢

@sabitertan thank you 🙏

sabitertan commented 5 years ago

I couldn't replicate this issue using "yarn". But if I use "npm i && npm run build && npm run serve" that throws error and crashes server. Can you confirm you are installing and running with only "yarn"?

francisrod01 commented 5 years ago

@marinav it's a good practice of many developers to isolate the problem then trying to share the doubt or problem with someone using codesandbox, for example.

Many times we discover the problem coding it again.

Currently we don't know the problem and it make the situation too hard to help.

marinav commented 5 years ago

Hi,

Thank you for you answers !

@sabitertan yes I'm installing and running with only "yarn".

@francisrod01 I understand, I created a codeSandbox here I just restarted from your project and updated getCurrentProfile function to make an api call (like I did above), I see the name in the profile page but I when I do a curl on my sandbox page :

curl https://30v6849055.codesandbox.io/profile/2

I don't see the html of my page (unlike it is in local when I do a yarn build and serve) so it's hard to say if it's working or not.

francisrod01 commented 5 years ago

I just forked your codesandox example and I typed curl on the console and I could see the response.

curl https://4jj997n9x.codesandbox.io/profile/2

Below I present a screenshot about that:

image

marinav commented 5 years ago

ok sorry I didn't express myself well, when I curl codesansbox url I have the same result as you but I don't see body's html. While in local when I just clone the project and modify the function I see this :

Orignal function curl result: Capture d’écran 2019-03-25 à 16 00 51

With api call (same as codeSandbox) curl result : Capture d’écran 2019-03-25 à 15 59 39

I think it maybe comes from react-frontload because in loader.js file, after frontloadServerRender function when I get the state with store.getState(), I get an empty state with my custom function. And it's not the case with the orignal function witch get profile data