简体   繁体   中英

React Server Side Rendering - how to render from the server with :productId params passed in?

I have a React app that is rendered on the client and the server (node and express). I am trying to get the rendering to work if someone types in the url http://webaddress.com/products/1 . If I type this in and hit enter (or refresh the page), the application crashes because it doesn't know how to get the 1 in the url to parse and get the proper product.

If I click on a link in a navigation that links to products/1, the application works fine and the proper product is displayed.

How do I get React-Router to get the :productsId (the 1 in /products/1) param from the url that the visitor types in http://webaddress.com/products/1 ?

Here's my server.js:

import express from 'express';
import http from 'http';

var PageNotFound = require('./js/components/PageNotFound.react');

import React from 'react';
import { renderToString } from 'react-dom/server';
import { match, RouterContext } from 'react-router';

import { routes } from './routes';

const app = express();

app.use(express.static('public'));

app.set('view engine', 'ejs');

/* We call match(), giving it the routes object defined above and the req.url, which contains the url of the request. */

app.get('*', (req, res) => {
  // routes is our object of React routes defined above
  match({ routes, location: req.url }, (err, redirectLocation, props) => {
    if (err) {
      // something went badly wrong, so 500 with a message
      res.status(500).send(err.message);
    } else if (redirectLocation) {
      // we matched a ReactRouter redirect, so redirect from the server
      res.redirect(302, redirectLocation.pathname + redirectLocation.search);
    } else if (props) {
      // console.log("props on server.js: ", props);
      // if we got props, that means we found a valid component to render
      // for the given route. renderToString() from ReactDOM takes that RoutingContext
      // component and renders it with the properties required.
      const markup = renderToString(<RouterContext {...props} />);

      // render `index.ejs`, but pass in the markup we want it to display
      res.render('index', { markup })

    } else {
      // no route match, so 404. In a real app you might render a custom
      // 404 view here
      console.log("not found page");
      res.sendStatus(404);
      // respond with html page
       if (req.accepts('html')) {
         res.render('404', { url: req.url });
         return;
       }

       // respond with json
       if (req.accepts('json')) {
         res.send({ error: 'Not found' });
         return;
       }

       // default to plain-text. send()
       res.type('txt').send('Not found');
    }
  });
});

const server = http.createServer(app);

app.set('port', (process.env.PORT || 3000))

app.get('/', (req, res) => {
  var result = 'App is Running'
  res.send(result);
}).listen(app.get('port'), () => {
  console.log('App is running, server is listening on port', app.get('port'));
});

Here's the routes.js file:

import TopLevelContainerApp from './js/components/TopLevelContainerApp.react'
import Home from './js/components/Home.react';
import Product from './js/components/Product.react';

const routes = {
  path: '',
  component: SolidBroadheadApp,
  childRoutes: [
    {
      path: '/',
      component: Home
    },
    {
      path: '/products/:productId',
      component: Product
    }
}
export { routes };

Here is the client side rendering js:

import React from 'react';
import ReactDOM from 'react-dom';
import { Router, browserHistory } from 'react-router';

import { routes } from './../routes';

ReactDOM.render(
  <Router routes={routes} history={browserHistory} />, document.getElementById('website-app')
);

Here is the ProductStore.js:

var AppDispatcher = require('../dispatcher/AppDispatcher');
var EventEmitter = require('events').EventEmitter;
var assign = require('object-assign');

var articles = null;
var links = null;
var product = null;

function setArticles(receivedArticles) {
  articles = receivedArticles;
  return articles;
}

function setLinks(receivedLinks) {
  links = receivedLinks;
  return links;
}

function setProduct(productId) {
  console.log("products store productId: ", productId);
  function filterById(obj) {
    return obj.id === productId;
  }

  var filteredArticlesArr = articles.filter(filterById);
  product = filteredArticlesArr[0];
  return product;
};

function emitChange() {
  ProductsStore.emit('change');
}

var ProductsStore = assign({}, EventEmitter.prototype, {

  addChangeListener: function(callback) {
    this.on('change', callback);
  },

  removeChangeListener: function(callback) {
    this.removeListener('change', callback);
  },

  getArticles: function() {
    return articles;
  },

  getLinks: function() {
    return links;
  },

  getProduct: function() {
    return product;
  }

});

function handleAction(action) {

  switch (action.type) {

    case 'received_products_articles':
    setArticles(action.articles);
    emitChange();
    break;

    case 'get_links':
    setLinks(action.articles);
    emitChange();
    break;

    case 'get_product':
    setProduct(action.productId);
    emitChange();
    break;
  }

}

ProductsStore.dispatchToken = AppDispatcher.register(handleAction);

module.exports = ProductsStore;

Here is the Product component that renders the specific product:

var React = require('react');
var ProductArticle = require('./products-solid-components/ProductArticle.react');

var ProductsStore = require('./../stores/ProductsStore');

    // gets all the products 
    var ProductsArticlesWebUtilsAPI = require('./../../utils/ProductsArticlesWebUtilsAPI');
    ProductsArticlesWebUtilsAPI.initializeArticles();

var Product = React.createClass({

  getInitialState: function() {
    return {
      articles: ProductsStore.getArticles(),
      product: ProductsStore.getProduct()
    };
  },


  componentDidMount: function() {
    ProductsStore.addChangeListener(this.onProductChange);
  },

  componentWillUnmount: function() {
    ProductsStore.removeChangeListener(this.onProductChange);
  },

  onProductChange: function() {
    this.setState({
      articles: ProductsStore.getArticles(),
      product: ProductsStore.getProduct()
    });
  },

  render: function() {

    if (this.state.articles)
    if (this.state.product) {

      return (
        <div className="product">
          <section className="container-fluid">
            <ProductArticle name={this.state.product.name} largeImage={this.state.product.largeImage} description={this.state.product.description} />
          </section>
      );

    } else {
      return (
        <div>Product is on the way</div>
      );
    }


  }
});

module.exports = Product;

Here's the file that gets the info:

var ProductsActionCreators = require('../js/actions/ProductsActionCreators');

var productArticles = [
  {
    "id": 1,
    "name": "product 1",
    "largeImage": "1.png",
    "largeVideo": "1.mp4",
    "description": "1 description",
  },
  {
    "id": 2,
    "name": "product 2",
    "largeImage": "2.png",
    "largeVideo": "2.mp4",
    "description": "2 description",
  },
  {
    "id": 3,
    "name": "product 3",
    "largeImage": "3.png",
    "largeVideo": "3.mp4",
    "description": "3 description",
  },
];

var products = [];

function separateProductIdsAndNamesOut(productArticles) {
  console.log("productArticles: " + productArticles);
    products = productArticles.map(function(product) {
    return { id: product.id, name: product.name };
  });
  return products;
}

function initializeArticles() {
  return ProductsActionCreators.receiveArticles(productArticles);
}

// to build links in a nav component without payload of video and large img etc
function initializeProductsForNav() {
  return ProductsActionCreators.receiveArticlesIdsAndNames(separateProductIdsAndNamesOut(productArticles));
}

module.exports = {
  initializeArticles: initializeArticles,
  initializeProductsForNav: initializeProductsForNav
};

UPDATE: The console.log from server.js when I type in the url manually or hit refresh once on the page:

props on server.js:  { routes: 
   [ { path: '', component: [Object], childRoutes: [Object] },
     { path: '/products/:productId', component: [Object] } ],
  params: { productId: '4' },
  location: 
   { pathname: '/products/4',
     search: '',
     hash: '',
     state: null,
     action: 'POP',
     key: '8b8lil',
     query: {},
     '$searchBase': { search: '', searchBase: '' } },
  components: 
   [ { [Function] displayName: 'SolidBroadheadApp' },
     { [Function] displayName: 'Product' } ],
  history: 
   { listenBefore: [Function],
     listen: [Function],
     transitionTo: [Function],
     push: [Function],
     replace: [Function],
     go: [Function],
     goBack: [Function],
     goForward: [Function],
     createKey: [Function],
     createPath: [Function],
     createHref: [Function],
     createLocation: [Function],
     setState: [Function],
     registerTransitionHook: [Function],
     unregisterTransitionHook: [Function],
     pushState: [Function],
     replaceState: [Function],
     isActive: [Function],
     match: [Function],
     listenBeforeLeavingRoute: [Function] },
  router: 
   { listenBefore: [Function: listenBefore],
     listen: [Function: listen],
     transitionTo: [Function: transitionTo],
     push: [Function: push],
     replace: [Function: replace],
     go: [Function: go],
     goBack: [Function: goBack],
     goForward: [Function: goForward],
     createKey: [Function: createKey],
     createPath: [Function: createPath],
     createHref: [Function: createHref],
     createLocation: [Function: createLocation],
     setState: [Function],
     registerTransitionHook: [Function],
     unregisterTransitionHook: [Function],
     pushState: [Function],
     replaceState: [Function],
     __v2_compatible__: true,
     setRouteLeaveHook: [Function: listenBeforeLeavingRoute],
     isActive: [Function: isActive] } }
this.state.searchResult:  null
this.state.productLinks in component:  null

UPDATE 2: I have removed 404 and splat routing and the server console.log shows:

App is running, server is listening on port 3000
props.params on server.js:  { productId: '4' } // looks good; proper productId is passed.
this.state.productLinks in component:  null
this.props in product component:  { productId: '4' } // looks good; should render properly
props.params on server.js:  { productId: 'build.js' } // why is this assigned 'build.js' causing the problem???
this.state.productLinks in component:  null
this.props in product component:  { productId: 'build.js' } // why is this assigned 'build.js'? This is probably the cause of the problem.

The process seems to run twice, the first time its assigned the proper id, the second time, it's assigned the 'build.js' that is built by webpack? WTF.

The gist that shows the props being rewritten from productId to 'build.js': https://gist.github.com/gcardella/1367198efffddbc9b78e

Webpack config:

var path = require('path');
var webpack = require('webpack');

module.exports = {
  entry: path.join(process.cwd(), 'client/client-render.js'),
  output: {
    path: './public/',
    filename: 'build.js'
  },
  module: {
    loaders: [
      {
        test: /.js$/,
        loader: 'babel'
      }
    ]
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: JSON.stringify('production'),
        APP_ENV: JSON.stringify('browser')
      }
    })
  ]
}

Update 3: I fixed the double cycle by removing the check for state, but this fails when rendering server side because the state is not set yet. So I add a check to see if state.product exists in the product component:

var React = require('react');
var ProductArticle = require('./products-solid-components/ProductArticle.react');
var ProductPresentation = require('./products-solid-components/ProductPresentation.react');
var ProductLargeImage = require('./products-solid-components/ProductLargeImage.react');

var LargeLeftImageArticle = require('./reusable-tool-components/LargeLeftImageArticle.react');
var LargeRightImageArticle = require('./reusable-tool-components/LargeRightImageArticle.react');

var ProductsStore = require('./../stores/ProductsStore');

// if (process.env.APP_ENV === 'browser') {
    var ProductsArticlesWebUtilsAPI = require('./../../utils/ProductsArticlesWebUtilsAPI');
    ProductsArticlesWebUtilsAPI.initializeArticles();
// }

var Product = React.createClass({

  getInitialState: function() {
    return {
      articles: ProductsStore.getArticles(),
      product: ProductsStore.getProduct()
    };
  },


  componentDidMount: function() {
    ProductsStore.addChangeListener(this.onProductChange);
  },

  componentWillUnmount: function() {
    ProductsStore.removeChangeListener(this.onProductChange);
  },

  onProductChange: function() {
    this.setState({
      articles: ProductsStore.getArticles(),
      product: ProductsStore.getProduct()
    });
  },

  render: function() {
    console.log("this.props in product component: ", this.props);

    // if (this.state.articles)

      console.log("after check for this.state.product on component this.props.params.productId: ", this.props.params.productId);
      if (this.state.product) {
      return (
        <div className="product">
          <section className="container-fluid">
            <ProductArticle name={this.state.product.name} largeImage={this.state.product.largeImage} description={this.state.product.description} />
          </section>
        </div>
      );
    } else {
      console.log("no state");
      return (
       // if there is no state, how do I render something on the server side?
      );
    }
  }
});

module.exports = Product;

It turned out to not be a routing issue at all. There were many problems in different areas:

I had to rename my bundle.js to another name such as client-bundle.js .

Then I had to fix a problem in my ProductsStore to filter the array of products to match the productId:

function setProduct(productId) {
  var filteredProductsArr = products.filter(function(product) {
    return product.id == productId;
  });

  product = filteredProductsArr[0];
  return product;
};

Then I had to put an action creator in my Product component's componentDidMount(): ProductsActionCreators.getProduct(this.props.params.productId);

All this and it works like a charm :)

You should be able to retrieve this using

this.props.params.productId

A good example for this: https://github.com/rackt/react-router/blob/master/examples/query-params/app.js

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM