Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SSR basic api #58

Merged
merged 5 commits into from Dec 15, 2018
Merged

SSR basic api #58

merged 5 commits into from Dec 15, 2018

Conversation

macrozone
Copy link

@macrozone macrozone commented Oct 13, 2018

(see #10 (comment))

To do SSR with FlowRouter, only a few pieces are missing and I try to add them here.

Basically with meteor, SSR works like this (react example)

import React from "react";
import { renderToString } from "react-dom/server";
import { onPageLoad } from "meteor/server-render";

import App from "/imports/Server.js";

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

The sink parameter will contain all request params (path, queryParams, etc.). With react-router you usually control the rendered component inside of <App /> depending on the location passed.

However with FlowRouter our routes usually look like this:

FlowRouter.route('/users/:userId', {
    name: 'about',
    action({userId}) {
      // using react-mounter 
      mount(MainLayout, {
        content: () => <UserPage userId={userId} />,
      });
    },
  });

Notice: mount(Component, props) from above basically does: <Component {...props} /> and injects that on the body on the client.

Ok, so if we could somehow call the right action definition inside of a onPageLoad(sink => { ..}) callback and just call renderToString from the action, we were done!

So the idea would be:

onPageLoad(sink => {
    // 1. find the right route from FlowRouter._routes that matches the requested path (sink.request.url.path)
   // const route = FlowRouter._routes.find( ....)
   // 2. call its action (ignore the params and queryParams for the moment)
   route.action(params, queryParams)
   // somehow make route.action call sink.renderIntoElementById
  
}

I implemented Piece 1 in this PR:


  const result = FlowRouter.matchPath(
    sink.request.url.path,
    sink.request.query
  );
  if (result) {
    const { route, params } = result;
  }
    
   

matchPath will look through all routes defined and return {route, params} if found, or null otherwise. route is the flow router route definition and params are the path params. E.g. in our example above: if the route has this definition /users/:userId' and /users/1234 is requested, params will be {userId: "1234"}

with this we can call our action:

   // ... 
    const { query } = sink.request;
    const data = route.data ? route.data(params, query) : null;
    route.action(params, query, data);

Notice: data is an optional feature from flow-router, and can be ignored in this example.

ok, now how to get action to call sink.renderIntoElementById on the server?

One idea would be to pass sink to the action in a 4th argument:

route.action(params, query, data, { sink });

but then you need to adjust all your actions, which is not really elegant.... :-/

Ok what other api would be possible? Let's have a look how you usually render content with blaze:

// blaze example
FlowRouter.route("/", {
  name: "home",
  action() {  
     this.render("home")
  }
}

Attaching the render function to this is a good idea. Let's use such an api. Luckily, we can easily replace the render function with our own:

// client/main.js
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { mount } from 'react-mounter';

FlowRouter.Renderer = {
    render: mount
}

this makes the router work on the client again using react-mounter. In our routes definition we can use this api like this:


FlowRouter.route('/users/:userId', {
    name: 'about',
    action({userId}) {
      // replacing mount from react-mounter with this.render
      this.render(MainLayout, { 
        content: () => <UserPage userId={userId} />,
      });
    },
  });

Now on the server, we need to directly replace .render on a route using sink like this:

onPageLoad(sink => {
  const result = FlowRouter.matchPath(
    sink.request.url.path,
    sink.request.query
  );
  if (result) {
    const { route, params } = result;
    // NEW: 
    route.render = (Component, props) => {
      sink.renderIntoElementById('react-root', renderToString(<Component {...props} />);
    };
    const { query } = sink.request;
    const data = route.data ? route.data(params, query) : null;
    route.action(params, query, data, { sink });
})

Voilà.

Some notes:

  • i added some additional apis on the server version of flowrouter, because i used that in my react-component
  • react-mounter itself could do server side mounting, but it is restriced to kadira:flow-router-ssr and as both projects are abandoned, its probably better to fully replace react-mounter (even on the client)
  • above code with the sink could also be included in FlowRouter, but i maybe do that in another PR
  • rendering on the server has pitfalls, carefully test your app through

@macrozone macrozone changed the title WIP: add new functions to facilitate SSR WIP: SSR Oct 13, 2018
@macrozone macrozone changed the title WIP: SSR SSR basic api Oct 14, 2018
@macrozone
Copy link
Author

here is the code i used in my app to do SSR with flow router:

https://gist.github.com/macrozone/637f66dbac7bf752b0814cdc0699b677

there are some pitfalls:

  • react-loadable, is solved by captainN: https://github.com/CaptainN/meteor-react-starter
  • fastrender: https://github.com/abecks/meteor-fast-render/ is needed, because it tracks subscriptions and injects data back in. otherwise you will end up with flashing containers from has-data --> loading --> hasData
  • helmet: see also captainN's sample that can even stream the html to the client (in my example i send the full string at once)
  • dealing with random stuff: calculating a random number or string without a seed will lead to differences in server and client. I injected a seed that is available both in client and server and use that to generate random ids (with seedrandom or similar)
  • be careful when you rely on browser' globals like window, navigator, document, etc.

@macrozone
Copy link
Author

i also need to revisit/ remove the .current() api from the server side version, because the server side router has to be stateless, otherwise, this leads to ugly concurrency problems

@macrozone
Copy link
Author

I had also another concurrency problem with react.createContext, but i think i have solved it now... facebook/react#13854

@macrozone
Copy link
Author

any feedback would be much appreciated so that this can be merged. I used it in production for some time now

@dr-dimitru
Copy link
Member

Hello @macrozone ,

I'm very sorry for delayed response, end of the year.... 🤦‍♂️
Thank you for putting this up together.
I've went through all other related issues at meteor and react, you've done gj there 💪🎉

I was about to merge this PR, doing maintenance routine before merge, like running tests, but end up looking at this issue — #59 mb you have any idea around this?

@coagmano
Copy link

@dr-dimitru I just tried checking out this PR and all 143 tests pass 👍

@dr-dimitru
Copy link
Member

Hello @coagmano ,

Thank you for the quick update.
Wondering why this even raised here, let's continue at #59

@dr-dimitru dr-dimitru merged commit a6218f1 into veliovgroup:master Dec 15, 2018
dr-dimitru added a commit that referenced this pull request Dec 16, 2018
 - 📦 NPM `page@1.9.0` update
 - 🤝 Merge #58, introducing Basic SSR API, partly 🤔 covering #10,
thanks to @macrozone for PR and @coagmano for assistance
@pmogollons
Copy link

Hi @macrozone, I have been trying to use the info on this PR and the gist you posted but haven't been able to get SSR working. Do you have a repo I can checkout to check how to get working SSR with FlowRouter?

Thanks.

@pmogollons
Copy link

Just got it working. Unfortunately, I use grapher-react and still don't have SSR support. I'll try to get SSR working with it and create a tutorial for doing it.

@dr-dimitru
Copy link
Member

Hello @pmogollons 👋

Sorry I can't help you with SSR as I have no experience with it. If you're looking for implementing SEO properly, I recommend to take a look on prerendering.com

@pmogollons
Copy link

@dr-dimitru Thanks, I think we will try ostrio to get SEO faster.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants