veliovgroup / flow-router

🚦 Carefully extended flow-router for Meteor
https://packosphere.com/ostrio/flow-router-extra
BSD 3-Clause "New" or "Revised" License
202 stars 29 forks source link

SSR basic api #58

Closed macrozone closed 5 years ago

macrozone commented 6 years ago

(see https://github.com/VeliovGroup/flow-router/issues/10#issuecomment-429559609)

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:

macrozone commented 6 years ago

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:

macrozone commented 6 years ago

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 commented 6 years ago

I had also another concurrency problem with react.createContext, but i think i have solved it now... https://github.com/facebook/react/issues/13854

macrozone commented 5 years ago

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

dr-dimitru commented 5 years ago

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 — https://github.com/VeliovGroup/flow-router/issues/59 mb you have any idea around this?

coagmano commented 5 years ago

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

dr-dimitru commented 5 years ago

Hello @coagmano ,

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

pmogollons commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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