How to use server-render in a promise function


#1

Hello there, I tried to initiate Server Side Rendering with server-render package from @benjamn and this worked well until I try to use it with Universal Router.

My problem is on the server you guess:

main.js (server)

import { Meteor } from 'meteor/meteor';
import React from "react";
import { renderToString } from "react-dom/server";
import { onPageLoad } from "meteor/server-render";
import UniversalRouter from 'universal-router/legacy';
// legacy because node version of meteor makes it crash otherwise

import { App } from '../imports/App';
import { Big } from '../imports/Big/Big';
import { Fourofour } from '../imports/404/404';


Meteor.startup(() => {
  // code to run on server at startup
});

const routes = [
  {
    path: '/',
    action() {
      return {
        title: 'App',
        component: <App />
      };
    }
  },
  {
    path: '/about',
    action() {
      return {
        title: 'About us',
        component: <Big />
      };
    }
  },
  {
    path: '*',
    action() {
      return {
        title: '404',
        component: <Fourofour />
      };
    }
  }
];
const router = new UniversalRouter(routes);
function renderLocation(thisSink) {
  router.resolve({path: thisSink.request.url.path}).then(route  => { 
    console.log (route.component)
    console.log(route.title)
      //This is working and returning valid title and component

  thisSink.renderIntoElementById("app", renderToString(
    <Big />
  ));
      // NOT WORKING !! sink.renderIntoElementById
     // will fail silently in this context (no error in the console)
  });
  thisSink.renderIntoElementById("app", renderToString(
    <Big />
  ));
     // This is working here !!
}

onPageLoad(sink => {
  //Wrap all the code in onPageLoad method just to be able to rerun it and generate new static HTML on every page request
  renderLocation(sink);
});

As you can see I cannot render my hard-coded ‘<Big / >’ component because server-render fails to execute within the promise.

What to do??


#2

OK I feel terrible because I didn’t know how to do async work with this (it was documented though).

I leave my snippet here in case someone else has the same problem:

import { Meteor } from 'meteor/meteor';
import { renderToString } from "react-dom/server";
import { onPageLoad } from "meteor/server-render";
import UniversalRouter from 'universal-router/legacy';
import routes from '../both/routes'

const router = new UniversalRouter(routes);

function renderLocation(thisSink) {
  router.resolve({path: thisSink.request.url.path}).then(route  => {
    console.log(route.title);
    thisSink.renderIntoElementById("app", renderToString(
      route.component
    ));
  });
}
onPageLoad(async sink => {
  //generate new static HTML on every page request
  await renderLocation(sink);
});

#4

Hi
I was facing a Meteor.subscribe error, on trying to use the server-render package with React Router v4.
This is what my server side code looks like

onPageLoad((sink) => {
  const context = {};

  const App = props => (
      <StaticRouter location={props.location} context={context}>
        <Routes/>
      </StaticRouter>
  );

  sink.renderIntoElementById('app', renderToString(<App location={sink.request.url} />));
});

And this is the client side and the subscriptions:

import React from 'react';
import { Meteor } from 'meteor/meteor';
import { render } from 'react-dom';
import {RenderRoutes} from '../imports/api/routes.jsx'
import { onPageLoad } from 'meteor/server-render';
import ReactDOM from 'react-dom';

import {
  Router,
  Route,
  Link,
  Switch
} from 'react-router-dom'
import createBrowserHistory from 'history/createBrowserHistory'

const history = createBrowserHistory()

const Application = () => (
    <Router history={history}>
      <RenderRoutes/>
    </Router>
);

onPageLoad(()=> {
  ReactDOM.render(<Application/>, document.getElementById('react-root'));
});
export default createContainer(() => {
  const handle1 = Meteor.subscribe('categories');
  const handle2 = Meteor.subscribe('subcategories');
	const handle3 = Meteor.subscribe('products');
  const isReady1 = handle1.ready()
  const isReady2 = handle2.ready()
  const isReady3 = handle3.ready()
  return {
    products: isReady3 ? Products.find({}).fetch() : [],
    categories: isReady1 ? Categories.find({}).fetch() : [],
    subcats: isReady2 ? SubCategories.find({}).fetch(): [],
  };
}, B2C);

The create container is creating problems, since, the above code worked for a very simple App with no containers or subscriptions

I am getting an error such as:
TypeError: Meteor.subscribe is not a function
in the terminal
Would you know what is happening or what mistake am I making


#5

Looks like you’re trying to use Meteor.subscribe on the server.


#6

Hi tomsp
For the server render, is not everything from the client going to be processed in a “server” way, and since all collections are already present with the server, won’t it ignore the Meteor.subscribe?

Or is there some other way to handle the createContainer part in the server-render
I was not able to figure out in the docs
Thanks


#7

It’s working when autopublish is turned on and on removing the subscriptions from the client side
Which i feel is a bit improper a solution, i do not wish to publish all data everywhere, there should be a workaround right?


#8

The problem is resolved.
I put all the subscriptions in Meteor.isClient block, so yeah the subscriptions shouldn’t run on the server at all, even if the code is present on the client and one is using it for server-render, the Meteor.isClient block should be provided for the codes which might cause error on server render


#9

Glad you figured it out! My answer was going to be: you have to return a Promise from the callback function passed to onPageLoad if you want the server to wait for async work to finish.

In case you’re still having problems, I think you may need to return a Promise from your renderLocation function, rather than just calling router.resolve:

function renderLocation(thisSink) {
  return router.resolve(...);
}

#11

Thanks @benjamn, indeed this was the trick to make server-render work with universal router on the new Meteor version.
I used not to return router.resolve(..) and just call router.resolve(..) in renderLocation(sink) which worked OK on Meteor 1.5 but caused performance issue on 1.6 for some reason.

The simple trick to return the promise works super good, and is the way we should work with async/await: the await renderLocation(sink) means ‘await this Promise’ so make sure to return the promise in the renderLocation function :slight_smile: