- This repo provides some information about SSR, why we use it, and challenges of SSR.
- I will use React, with redux and React Router, Also, topics like authentication will be discussed.
- Check These Links:
check these links one, two for more information about the topic.
These notes and code are from various sources, but most of it are from Stephen Grider's course
-
How traditional React app works?
- The browser makes a request to the server (hosting the app), and an html document is returned by the server
- The html document has a script tag with
src
to the entire react app as a bundle (bundle.js).
- Traditional React apps take a slightly long time to get content visible on the screen, which is a behavior we want to avoid.
- With SSR, whne the browser reach out to server, it will get an html document that has much more content compared to the traditional react app. So, having a full document really get content to screen pretty fast. (We are still using react on the client).
- We will have
two
separate backend servers, one for business logic (auth, logging, db access, ...) and the other one for rendering the html document. - the business logic server lives at
https://react-ssr-api.herokuapp.com/
- the first benefit of this separation is that we could replace react with any other technology at any time.
- Another benefit is that we could scale out each server separately.
- IMPORTANT NOTE: SSR performance is not fast and needs optimization (like having fast machines for the rendering servers). check out this Walmart labs blog post
- ReactDOM.render: Creates instances of a bunch of components and mounts them to a DOM node.
- ReactDOM.renderToString: renders a bunch of components one time and produces a string out of all the resulting HTML.
- Isomorphic/Universal Javascript: same code runs on both client and server (like using es5 imports on the server instead of
require
) - Server-side Rendering: Generate HTML on the server and ship it down to the client.
- Event handlers (onClick, ...) are registered after component is rendered and atached to the dom in a regular react app.
- But in the case of SSR, no JS files are sent to the user by default, So, we need, after sending HTML, to send our react app that have the JS functionality.
- So to ship the react app to the browser we are going to create a second bundle that only contains the React App code (the first one contains server code + react app).
- These are the two bundles we create right now.
indexjs
serves as the root (bootup) for the react app on the server, andclientjs
serves as the root (bootup) for the react app on the browser.
-
indexjs
on the server depends on theHome.js
component, and also theclientjs
file depends onHome.js
component, but we need the segregation between them for many reasons, the first one being server code might have sensitive data that cannot be sent to the client -
Another reason is when we add Router and Redux, we will need different configurations for them when executed on the server or on the client.
-
the process of putting functionality back to an html that's already rendered is called Hydration.
- We have two tiers of routing inside our app like shown in th picture below
- express route handler will delegate requests to react router instead of handling it. So react router has the final saying in what gets shown on the screen. (both on server -delegating reqs to it by express handler- and on client when hydration occur).
-
So browser router looks at address bar in the browser, This is hardcoded in it, So we can't use it inside our sevrer.
-
We will use
StaticRouter
when we need routing on the server, andBrowserRouter
when the application gets hydrated on the browser. -
the setup will look like this:
-
Routes.js
: Will include all route mappings. -
Import this
Routes.js
file into:rerenderer.js
on the server - This usesStaticRouter
.client.js
on the client/browser - This usesBrowserRouter
.
Html Mismatch occurs when a mismatch occur between what gets produced on the client and on the server, so the html from server is different from what react app produces on the browser.
- We have four big challenges with regard to Redux with SSR.
- Challenge 1: the first one is a result of the other three challenges and should be solved by having two stores one on the server and the other on the client.
- Challenge 2: We need to know who is authenticated, and we are using cookie based auth, and this data is hard to obtain on the server.
- Challenge 3: on the browser, we call
actionCreator
, and let it do its thing (fire request, signal it has finished, run reducer, react re-renders when we state change ), but now on the server we need to handle all these steps that were handled for us on the browser. - Challenge 4: this is the easier one :)
- babel-polyfill is a module that enable using async/await syntax inside am action creator
- right now on the browser this is the flow (normal react flow)
- but on the server, we respond immediately after rendering app without waiting for the reducer to run and re-render the app, So the
componentDidMount
lifecycle method does not get called on the server.
- try to render the app two times:
- This solution has a pro of not having to write a lot code (however we need to know when action creator finishes)
- But, there are some cons:
- Rendering the app twice (takes a lot of resources to render a react app)
- This solution only works with one round of requests. (So a scenartio like requireAuth -> fetch Resource that require auth would not work.)
-
This solution has a con that we need to write more code.
-
But the Pros are:
- only render app one time.
- Makes data required by each component clear.
-
Note that for us to know what component will be rendered from the url we can not rely on the current Router (StaticRouter) configuration we have, because Router needs the app to be rendered first and that we don't need.
-
react-router-config is a library that allow us to inspect what components are needed for a requested page before the app gets rendered.
-
We will define a
loadData
function on each component that preloads any data that a component needs before being rendered
- there is an issue that rises from the fact that we create and populate a store on the server, but when bundle is sent to client the browser creates another empty redux store, causing an error to be logged in the browser console (becuase empty store clears page temporarily).
- the solution simply is to dump server store state into the HTML template and when bundle is shipped to the browser it will use the dumped data inside the HTML to init the browser store with it.
-NOTE: React render function by default serialize and sanitize user input, but in case of SSR we are sending content as string without sanitizing anything (Use serialize-javascript instead of json.stringfy)
check this link
- Our API uses cookies after going through OAuth flow, but the issue with cookies is that cookies are associated with full domain, so requests to sub domains will not include the cookie. So our Render Server will not be able to make requests to API server on behalf of the browser.
- one solution to setup a proxy on the render server itself, and when a user wants to authenticate with the API server , they will go through that proxy first.
-
Note that the initial page loading does not require the proxy (render server will make the request on behalf of the browser.)
-
Note Reaching out to API is done via
Action Creators
and we have to make sure that the exact same action creators are used both the server (no proxy) and on the client (through proxy) - This is what Isomorphic javascript is all about.
remember that jwts could be used inside a cookie, but here I am discussing JWTs inside header, url or body of request.
-
The idea of SSR to send rendered content as fast as possible and using jwts will not satisfy this goal, as we would send req to server, then it will ask for a jwt, then we make another follow up request to send the jwt..., and when we think of the first initial request to the server, we would not be able to attach such a jwt and get content right away.
-
Also think of a scenario that a user tpe the url in the browser bar and hits enter, we have zero control on that request except that cookies are sent by default with any request.
-
So, The steps we will go through are:
- setup the proxy server.
- Make sure action creators called from server does not go through proxy.
- Make sure action creators called from browser go through proxy.
- step two diagram:
- step three diagram:
How Would be able to achieve these steps without writing code like this inside action creator
if(running on server) {
axios.get('http://react-ssr-api.herokuapp.com/users')
}
else if (running on the browser) {
axios.get('/api/users/', { cookie: cookie }) // remember, any request to /api will go through the proxy
}
- This could be achieved using some features from
axios
andredux-thunk
libraries. Namely, create custom axios instance, and extraArgument to the thunk object.
- Figuring out when we need to send an error code back to the client we need to use the
context
property on theStaticRouter
, (This context is just a property, not to be confused with the react Context system).(context property does not exist on the browser router)
- The static router as shown above passes the context to every page getting rendered as a prop, after static router finish rendering we can check the context object if it has an error object (err obj assigned by for example NotFoundPage component)
How do we connect the Context object which communicates some error message to the response object from express which we use to send a statusCode??
- we will end up writing code like this in our index.js (express route handler)
const staticRouterContext = {};
const content = Renderer(req, store, staticRouterContext);
if (context.notFound) {
return res.status(404); // send status here
}
return res.send(content); // then send the content
- And inside a component like
NotFoundPage
, we will recieve thestaticRouterContext
object created above like this
const NotFoundPage = ({ staticContext = {} }) => { // default to {} because it does not exist on BrowserRouter
staticContext.notFound = true;
return <h1>Ops, page Not Found</h1>
};
-
Note the
Renderer
function is the one that responsible for communicating thestaticRouterContext
to our components. -
Context is also for redirects on the server (use
Redirect
component from react-router-dom)
- this will essentially cause an error to be thrown from the component's
loadData
function. And we have to think of a better error handling.
- Just wire up a
.catch(err => {})
to ourPromise.all(loadDataFunctionsPeomises)
inside server'sindex.js
file. - Not the best solution, actually it is the poorest one., because it dumbs the entire rendering process and just send an error to the user.
- Always attempt to render the content, no matter what error occurs. (
Promise.all(promises).then(render).catch(render)
) - Better from the previous, but there is a big issue here, and that is, we are using
Promise.all
, and if any promise rejects, we will call catch and render early, even if there are some unresolved promises in the promise.all.
- So, the issue with approach 2 is rendering early because of how
Promise.all
works, it would be great if we could wait for all promises to resolve/reject and then render our content. - one solution is to wrap each promise that gets produced from the
loadData()
call in another promise. This wrapper promise works as a watcher of theloadData
promise, and this wrapper will always resolve, indicating the status of the inner promise. - All these wrapper promises (outer promises) will be passed to the
Promise.all
call and as mentioned above these wrappers will always resolve, so we will not face the problem we had with promise.all previously.
-
When a user tries to access a protected page, we should try to autheticate them (requireAuth) and if auth failed, we should redirect them away from this page to ,say, the login page.
-
The error handling is happening when we actually render the react application (after fetching data occur (whether resolved or rejected))
-
the below diagram shows that we have separated data fetcing process from error handling process. And very importantly if an error occurs during data fetching, we are still rendering the application and only then we handle errors.
But Why ??? => Remember error handling should also be done on the client, in case a user visit a public initial page then tries to visit a protected page the react app on the browser should be the one handling errors.
So the benefit is that our error handling is going to work equivalently well on the server and the browser.
check this link
- function that takes a component and wrap it and return an enhanced/wrapped version of the input component.
- The idea with HOC is to enhance components with functions or data.
- These type of components are very common around auth, redirect and validation logic in react applications.
- When we use the Redirect component from react-router-dom on server side to redirect the user, remember we are using StaticRouter on the server, and any redirects are recorded on te context object of that router.
- the context object will have a
url
,location
andaction
properties if a redirect is attempted.
- Optimizing for SEO, using
meta
tags. Check The Open Graph Protocol. - we want to have the ability to change these met tags depending on the page the user is visiting.
- we will use
react-helmet
for setting these meta tags.
-
Benefits of RenderToNodeStream:
- returns a readable stream (instead of a string)
- Short TTFB time (Time to First Byte) which is used by search engines to rate our pages.
-
Why not use it? Ans.- Some features we use right now would not be available, like redirecting, There are some work arounds for it, but why go with a workaround :D
- Stick to the page approach which means, when ever a component is a root level or needs a preloading of data, it should be a page and any other reusable component is put inside the components folder