ProtoStarter: SSR rendering and routing starter project that is easy to use


#1

Hello there,

I have been working on my spare time on a Starter Project to make prototypes quickly running. But this does not fall short on functionalities: isomorphic routing, SSR, State management.

This focuses on Developer happiness and productivity (using what @manuel who made Viewmodel explained to me).

The file structure is very simple to understand. Even for designers

I kept it simple so you can expand in any direction easily.

That being said, this is opinionated about the technologies you should use, as the stack is the simplest I could find out there to quickly start a prototype project.

Using:

Missing: a specific styling solution (maybe going to use Aphrodite)

And maybe an alternative project with inferno as this might be quite easy to do with Viewmodel and this technology stack.

I hope you will make use of it.

PS: Any comment or feedback is welcomed


#2

You can include a mechanism to populate the initial data based on server values.

  1. Create a server file that returns the initial state of your app:
// /imports/server/initialState.js
const dataFromServer = {
  person: {
    name: "Bobby"
  }
};

export default dataFromServer;
  1. Create a file which will be called from both client (components) and server (router). It calls the server’s initial state with require because we don’t want to expose the internals of our app to the client. If it’s on the client it will return a global object (set in the page’s head by the router, see 3 below).
// /imports/initialState.js
let state;

if (Meteor.isClient) {
  state = window.initialState;
} else {
  state = require("./server/initialState").default;
}

export default state;
  1. Modify /server/router.js so it puts the initial state on the global object:
// /server/router.js
// rest of imports
import initialState from "../imports/initialState";

const router = new UniversalRouter(routes);

function renderPage(thatSink) {
  router.resolve({ path: thatSink.request.url.path }).then(route => {
    // rest of block
    const state = `<script>window.initialState = ${JSON.stringify(initialState)}</script>`;
    thatSink.appendToHead(state);
  });
}

// rest of file
  1. Load the initial state in your components:
// /imports/Person/Person.js
import initialState from "../initialState";
Person({
  name: "",
  created() {
    this.load(initialState.person);
  },
  render() {
    <div>
      Name: <input b="value: name" /> <br />
      <label b="text: 'Hello ' + name" /> <br />
      <a href="/about">About Us</a>
    </div>;
  }
});

#3

Is this initial state common for all users? as far as I can tell, there is no way at the moment to get any session info in Meteor at the server when using the sink object.


#4

I think you’re right :frowning:


#5

Oh that’s a caveat. @benjamn do you think this could be done and added in server-render?


#6

Meteor server-render package is really great and enabled me to achieve a first paint of less than 1.4 seconds (used to to be almost 4 seconds) so I’m very grateful that @benjamn took this initiative.

But it seems we’re running into a limitation with the current implementation. We’re unable to infer any user information at the sink object, ideally we would’ve access to the userId and the request cookies in the server sink object so we dynamically generate html based on their state and data. There is currently an open issue discussing the need of extending the sink object.

Meteor uses client localstorage and then sockets to store and communicate the user session however this context in not available when doing SSR. The fast-render package had a workaround for this limitation by grabbing the user localstorage token from the client and passing it as a cookie and then it uses this cookie to grab the user at the server.

Using the user token to fetch the user data when doing SSR has some security concerns that were discussed here, and here.

Emily Stark from MDG already pointed out 3 years ago that at some point cookies and local storage need to co-exist for Meteor SSR to work. Here is an excerpt from her blog post:

Meteor and cookies will almost certainly have a long and happy relationship in the future. We will likely use cookies to implement server-side rendering; when you visit a Meteor app at foo.com that renders templates on the server, it needs to know who you are so that it can render the templates with your data in it. The only way for the very first HTTP request to foo.com to know who you are? A cookie set on a previous visit to foo.com! Moreover, as discussed above, we can combine cookies with localStorage tokens to achieve some of the best security properties of both mechanisms.

Perhaps a good first step is to extend the sink object request to include the cookies information which would allow us to create a package that inject the user data into the sink object, this should not be hard to do, I think it’s single block of code that needs to be modified in the webapp_server.js file. In addition to the cookies, ideally a userId field would also be available in the sink object if the user is logged in which can be used to retrieve user specific data.

What do you guys think?


#7

I love the story but the best thing to do is just storing user data on the server instead of the database. Why a user cookie? Only to tell if the user can have automatic identification on the server at connection time


#8

just storing user data on the server instead of the database

I’m not really sure how is that possible or what would that solve? if I hit https://yourapp.com then how does the server SSR function know that I’m new user or returning user with an already logged in session? and if I’m already logged in, then how can the server know it’s me and not someone else?


#9

Maybe I misunderstood. I also agree that we need cookies to get the ‘token’ of the user and make sure to identify the user.

I thought the problem was about DB connectivity, but from what you said it is not the problem here.

I think your approach is reasonable !


#10

Just updated viewmodel.org with this project. I’ve been wanting to add SSR to the documentation for a long time. The only problem is that I had to move the Blaze docs to another domain (with a link in the main page). I wanted to keep both docs under the same roof but I don’t think I should wait until SSR for a hybrid app is available (if that ever happens).


#11

I’m glad it was helpful :slight_smile:
Just a remark but do you plan to transfer the old Help section to somewhere else? There was some good info in that, it would be a shame to lose it forever


#12

There’s a link to the old docs right where it used to be (in the header bar. .


#13

I am trying to do something more fancy:

Rendering data from any of my collections, isomorphic.

The problem is the server-side that doesn’t want to render (I get an error linked to Meteor’s promises)
while on the client it works fine.

@benjamn do you think of anything I can do to achieve that?

Bonus point if you come with an elegant solution to do it according to the user data

EDIT: I have this error:

W20170905-17:54:04.362(2)? (STDERR) C:\Users\me\AppData\Local\.meteor\packages\meteor-tool\1.5.1
\mt-os.windows.x86_32\dev_bundle\server-lib\node_modules\fibers\future.js:280
W20170905-17:54:04.363(2)? (STDERR)                                             throw(ex);
W20170905-17:54:04.364(2)? (STDERR)                                             ^
W20170905-17:54:04.365(2)? (STDERR) AssertionError: 1 === -1
W20170905-17:54:04.366(2)? (STDERR)     at C:\Users\me\AppData\Local\.meteor\packages\promise\0.
8.9\npm\node_modules\meteor-promise\fiber_pool.js:21:16

#14

In Universal Router I want to asynchronously pass my collection of Ads as a ‘props’ in order to access the collection stored in component’s state from server side (I hope this will fix the promise error).

{
    path: '/',
    async action() {
      const ads = await import('/imports/_Collections/Ads');
      return {
        title: 'myRouteTitle',
        component: <App data={ads} />
      };
    }
  }

I have one question: can I reuse ads from <App data={ads} /> inside my App’s ViewModel component and children ? if so in which vm property ?
Or Do I have to write this in plain React to get the props data?

Suggestions are welcome


#15
<App ads={ads} />

It will be in the property ads of App. See http://viewmodel.org/#BasicsState


#16

Hello everyone,

I am working on next steps for Protostarter.
I made Inferno-ProtoStarter, a version that is more advanced and showcase more features like nested routes and added client-side navigation between routes if javascript is enabled.

I think Inferno version will be the main one since there are so many problems with React licensing, and Inferno is actually faster than React.

I would like to showcase more features. The next steps are:

  1. Manage to pre-load DB on server so it serves a list of Ads at SSR time (+control performance of doing this) @alawi if you have a working code that demoes this I would be happy to get it :slight_smile:

  2. Add a form for users to add their own Ads.

  3. Remove insecure and autopublish packages, and manage methods accordingly

  4. Add simple User account management with Meteor account and a custom UI for it.

  5. Add Local storage / Cookies to identify the current user on the server at startup and SSR DB data accordingly

  6. Session storage Persistence : demonstrate how to persist user inputs (text area, checkboxes…) upon navigation (when changing routes and coming back - maybe a local minimongo will do it)

  7. Make routes filtered by user roles

  8. Infinite scrolling demo and other UI goodness demonstrations (Loading component, Modal dialog, animations, Tabs, Search with highlights…)

  9. Have a component written in Native Inferno, that would be integrated with VM components (to showcase that the hybrid approach is possible). @manuel if you already have some working code of this I would be happy to get it :slight_smile: .

  10. Have actual tests (unit and integration) -> I have no knowledge here

  11. Make it a PWA (Progressive Web App): Full offline capability, responsive…

I will also make an architecture pattern like Mantra that will go along with the ProtoStarter.

My goal is to have a generic, morphable project that demonstrates many use cases of current webapp needs, easy to get-going and add, remove or edit features following the architecture pattern.

VM is perfect for that. Thanks for making it @manuel

Of course, some help from the community is appreciated so if you show interest in this project and would like to help make this happen, please contact me and we can work together.

Ciao !


#17

The proper way to test a VM component is to test the component’s properties and methods, and that they’re properly bound to the template. I haven’t tried it but you should be able to instantiate an Inferno component the same way you do with a React one: var myComponent = new MyComponent();.

Testing the bindings requires a mechanism to inspect the rendered component. With React I use Enzyme:

  describe('bindings', ()=>{
    const rendered = shallow(<Person />);

    it('binds first name', ()=>{
      const elements = rendered.find('input[data-bind="value: firstName"]');
      expect(elements.length).toBe(1);
    });
  })

If you can do that with Inferno then testing is easy.


#18

Update on the topic:

New version of proto-starter is out, with React 16, Universal-router 4.2.1, etc… the first meaningful paint now happens at ~1500 ms from 4990 ms in previous version, huge improvement

I think I will stop iterating on Inferno and will go back on React as well, with this improvement and the new license it makes sense again, even if I don’t trust facebook that much on their intentions.

The Google Lighthouse audit has better numbers as well: on my crappy machine, for performance, I went from 32 to 65 ! (x2)

Well that’s a very good improvement with the new React 16 SSR.

I also added client-side navigation so when you click on About us it should feel way faster as well.

I hope people will make use of this as well


#19

Little comment:

I initially thought React 16 was now faster than Inferno, but this is actually not true. Inferno is still better.
I created the branch Inferno on the repo if you want to test: you will get around 1300 ms for Inferno vs 1500ms for React.