Server side rendering with Express, react-router-4 & redux for Meteor.
You can replace yarn by your favorite way of installing NPM packages.
To install yarn : https://yarnpkg.com/en/docs/install
To install "meteor yarn" : meteor npm i -g yarn
meteor yarn add react prop-types react-dom react-router-dom express helmet \
html-minifier react-helmet pino receptacle useragent redux react-redux moment \
i18next i18next-node-remote-backend i18next-xhr-backend react-i18next \
i18next-express-middleware serialize-javascript lodash actual is-retina
meteor add ssrwpo:ssr
git clone https://github.com/ssrwpo/ssr.git && cd ssr
meteor yarn install
cd demo
meteor yarn install
# For launching the demo in development mode
yarn dev
# For launching the demo in production mode
yarn prod
# For launching the demo with ios in simulator mode
yarn ios
# For launching the demo with android in simulator mode
yarn android
import { createRouter, logger } from 'meteor/ssrwpo:ssr';
...
createRouter({
// Your MainApp as the top component that will get rendered in <div id='react' />
MainApp,
// Optional: Store subscription
storeSubscription,
// Optional: An object containing your application reducers
appReducers,
// Optional: An array of your redux middleware of choice
appMiddlewares,
// Optional: An array of your collection names
appCursorNames,
// Optional: Add a redux store that watches for URL changes
hasUrlStore: false,
// Optional: An i18n config for client side
i18n,
// Optional: Server uses a platform transformer, client must load optional reducers
hasPlatformTransformer = true,
})
.then(() => logger.info('Router started'));
import { createRouter, logger } from 'meteor/ssrwpo:ssr';
...
createRouter({
// Your MainApp as the top component rendered and injected in the HTML payload
MainApp,
// Optional: Store subscription
storeSubscription,
// Optional: An object containing your application reducers
appReducers,
// Optional: An object containing the cursors required as data context
appCursors,
// Optional: A function that returns the content of your robots.txt
robotsTxt,
// Optional: A function that returns the content of your sitemap.xml
sitemapXml,
// Optional: A function that return the content of you humans.txt
humansTxt,
// Optional: An object with keys on URL with query parameters
urlQueryParameters,
// Optional: An object with keys on route solver
webhooks,
// Optional: An i18n config for server side
i18n,
});
logger.info('Router started');
By default this package logs for this package a muted. You can add asymetric
logging using an universal logger like pino
using the logger.set
method. The logger requires the following methods:
debug
, info
, warn
& error
which are used within this package.
We use i18next for server side rendered localization. It gets the user browser language and serves the right language with a default one(in case you don't serve for users one).
You can find more about :
Do not need it see FAQ how to remove from demo app.
react-router
will always render your application. For identifying a 404
, you
have to tell to the server that while rendering the app, one of the displayed
component is due to a 404
. This is achieved via the react-router
's staticContext
and by setting into it a has404
boolean used by the server to identify the route
as 404
Not found route.
Example: NotFound
To set up your robots.txt
, you need to have a key robotsTxt
inside the object
that you pass to the server-side createRouter function. This key should contain
a function that returns a string with the desired content of your robots.txt
.
The function receives the store as its first arguments. This allows you to
programmatically build your robots.txt
based on the store contents.
The same principle applies to humans.txt
and sitemap.xml
,
with the key humansTxt
and sitemapXml
respectively.
For example, you can populate your sitemap.xml of dynamic routes generated based on the store data. You can see examples of building these functions here:
NOTE For
sitemap.xml
we strongly recommend sitemap.js.
By passing a webhooks function, you can build your own server side routes powered by Express. A small example is setup in the demo: webhooks.
For the initial render, your app may require some defaults to ensure that it will server retina images or specific layout for a platform.
The platform
detection reducer provides the following platforms:
android
: Any tablet or phone with Android using Chrome or the built-in browser.ipad
: Any Apple iPad.iphone
: Any Apple iPhone.safari
: Any MacOS Safari (not iPhone or iPad).ie
: Any Internet Explorer before Edge.default
: All the other browsers and devices.By default, a platformTransformers
is provided and adds 4 built-in reducers to
the app: retina
, mobile
, viewportWidth
, viewportHeight
. It only applies
to server side rendering. When your client side app is rendered, you can patch
the default values that the server has injected with a bult-in component:
<BrowserStats retinaMinDpi={<number>} mobileBreakpoint={<number>} debounceTimer={<number>} />
where :
retinaMinDpi
: 144, by default (1.5 x 96 in dpi).mobileBreakpoint
: 992, by default (in px).debounceTimer
: 64, by default (4 x 16 in ms).If you want to build your own platformTransformers
and <BrowserStats />
, please
refer to the following sources for inspiration:
Each produced HTML payload is tagged with a build date allowing capabilities
to check if a reload is required. The reducer is named buildDate
and it
contains a UNIX date.
Store creation (see Reducer):
createCollectionReducers
createValueReducer
Actions on reducers:
collectionAdd
collectionChange
collectionRemove
valueSet
valueReset
When your collection is serialized in the store, you may want to synchronize it
when your application starts, or when entering a page, or on a user action ...
As this is a common use case for Meteor, we provide an easy way to create
mapDispatchToProps
methods for subscribing/subscribing or calling a validated
method that will synchronize your collection store.
Example: PubSub
createHandleSubscribe
The subscribe / unsubscribe based synchronization helper has the following API:
/**
* `createHandleSubscribe`
* Create an `handleSubscribe` function for your `mapDispatchToProps`.
* @param dispatch Store's dispatch.
* @param publicationName Your publication name which must accept an UNIX date value as `lastMod`.
* @param cursor A cursor on Mongo collection with a `lastMod` set on each item.
* @param valueStoreNameForSubscription Name of the value store identifying subscription state.
* @param collectionStoreName Name of the collection store holding replica of collection.
* @return A function allowing to subscribe and unsubscribe.
*/
createHandleSyncViaMethod
The validated method based synchronization helper has the following API:
/**
* `createHandleSyncViaMethod`
* Create an `handleSyncViaMethod` function for your `mapDispatchToProps`.
* @param dispatch Store's dispatch.
* @param validatedMethod A validated method, promised based
* (see didericis:callpromise-mixin) that accepts { lastMod } as its params.
* @param collectionStoreName Name of the collection store holding replica of collection.
* @return A function allowing to subscribe and unsubscribe.
*/
This package provides some HOC for common cases scenarios all geared torwards performances.
pure
Asymetric HOC for transforming a functional component into a React.PureComponent
on the client and leaving it unmodified on the server.
import React from 'react';
import { pure } from 'meteor/ssrwpo:ssr';
const MyComponent = (props) => ( ... );
export default pure(MyComponent);
Example: Performance
asymetricSsr
Same as pure
apart that it takes one or two component:
import React from 'react';
import { asymetricSsr } from 'meteor/ssrwpo:ssr';
...
const Loading = () => <p>Loading</p>;
const Loaded = () => <p>Loaded</p>;
...
const LoadingStateWithServerDisplay = asymetricSsr(Loaded, Loading);
const LoadingStateWithout = asymetricSsr(Loaded);
...
Example: Asymetric SSR
For optimal results, set your .babelrc
with the following content:
{
"presets": ["meteor"],
"plugins": [
"transform-class-properties",
"transform-react-constant-elements",
"transform-react-inline-elements",
"transform-react-remove-prop-types"
]
}
:warning: On Windows OS, these transforms could mess with your build process. A reduced set should be used with carefully chosen transforms. One contributors succeed to make the demo working on Windows using the following
.babelrc
:{ "presets": ["meteor"], "plugins": [ "transform-class-properties" ] }
For profiling the most appropriate libraries or function call a benchmark suite
is setup in benchmarks
. Launch a test using babel-node
.
Ex. babel-node benchmarks/getFromObject.js
The last request and last response received in Express are exposes by this
package as debugLastRequest
and debugLastResponse
, respectively. In Meteor's
shell, you can access them via:
import { debugLastRequest, debugLastResponse } from 'meteor/ssrwpo:ssr';
This project uses Jest and chai.
# In one-shot mode:
yarn test
# In watch mode:
yarn test.watch
The linter is based on ESlint configured with Airbnb styles.
yarn lint
:warning: All code must be linted before sending any PR. See the Contributing guide.
Logo created by Alexandre Tabbuso.