I was wondering if anyone else has had the problem where they have created a Server side rendering NodeJS app which works perfectly locally but then doesn't load server side once deployed to heroku.
I have created an app using Jared Palmer's awesome RazzleJS in combination with Redux, React Router and React Router Config.
The way it works is that in my server.js file I check the component that is loading for a static function called fetchData, if the function exists then the function is run which is a promise based request to an API using axios in a thunk.
In my server.js file another function then runs that checks all promises have completed before finally rendering the HTML for the page.
Locally this works perfectly and even with Javascript disabled the page is loading complete with the data fetched.
Having deployed this to heroku (single dyno - hobby plan) however if I disable javascript the page is loading with the data missing, suggesting that page is being rendered before the promises resolve. The data is then being loaded correctly using the equivalent ComponentDidMount dispatch for the data.
I currently have the following code:
Server.js
function handleRender(req, res) {
const sheet = new ServerStyleSheet();
const store = createStore(rootReducer, compose(applyMiddleware(thunk)));
const branch = matchRoutes(Routes, req.url);
const promises = branch.map(({ route }) => {
let fetchData = route.component.fetchData;
return fetchData instanceof Function
? fetchData(store, req.url)
: Promise.resolve(null);
});
return Promise.all(promises).then(() => {
const context = {};
const html = renderToString(
sheet.collectStyles(
<Provider store={store}>
<StaticRouter context={context} location={req.url}>
{renderRoutes(Routes)}
</StaticRouter>
</Provider>
)
);
const helmet = Helmet.renderStatic();
const styleTags = sheet.getStyleTags();
const preloadedState = store.getState();
if (context.url) {
res.redirect(context.url);
} else {
res
.status(200)
.send(renderFullPage(html, preloadedState, styleTags, helmet));
}
});
}
Example React Component
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchProductData } from '../thunks/product-data';
class Test extends Component {
static fetchData(store, url) {
store.dispatch(fetchProductData());
}
componentDidMount() {
if(this.props.productData.length === 0 ) {
this.props.fetchProductData() // Successfully fetches the data
}
}
render() {
return (
<div>
{ this.props.productData && this.props.productData.map( (product, i) => {
return <div key={i}>{product.title}</div>
})}
</div>
);
}
}
const mapStateToProps = state => {
return {
productData: state.productData
}
};
const mapDispatchToProps = dispatch => {
return {
fetchProductData(){
dispatch(fetchProductData());
}
}
};
export const TestContainer = connect(mapStateToProps, mapDispatchToProps)(Test);
This is just an example of the component layout as the ones I actually have are quite complex but in this instance productData would be set to [] in defaultState.
Also all reducers and actions are working correctly locally it is only when deployed to Heroku on a hobby plan that the server side rendering seems to no longer work?
So after a morning of research the reason that it wasn't working in my live environment was because I have a HOC wrapping component for the purposes of tracking analytics.
React-router-config however can't handle the fact that the fetchData function is another level deeper in the hierarchy and so all my promises were resolved with null.
Now that I have removed the HOC component again, server side rendering is once again working properly :)
Related
I got a problem with my dynamic route. It look like this
[lang]/abc
I am trying to get query value from [lang] but when I using useRouter/withRouter i got query during 2-3 render of page ( on first i got query.lang = undefined ). its possible to get in 1 render or use any technique ?
I found something:
isReady: boolean - Whether the router fields are updated client-side and ready for use. Should only be used inside of useEffect methods and not for conditionally rendering on the server.
https://nextjs.org/docs/api-reference/next/router#router-object
And the code would be like:
const router = useRouter();
useEffect(()=>{
if(!router.isReady) return;
// codes using router.query
}, [router.isReady]);
It's impossible to get the query value during the initial render.
Statically optimized pages are hydrated without the route parameters, so the query is an empty object ({}).
Next.js will populate the query after the page has been hydrated.
Next.js 10.0.5 and up
To determine if the route params are ready, you can use router.isReady inside a useEffect hook. For an example, see the answer provided by #doxylee.
Before Next.js 10.0.5
At first render of a dynamic route router.asPath and router.route are equal. Once query object is available, router.asPath reflects it.
You can rely on the query value within a useEffect hook after asPath has been changed.
const router = useRouter();
useEffect(() => {
if (router.asPath !== router.route) {
// router.query.lang is defined
}
}, [router])
GitHub Issue - Add a "ready" to Router returned by "useRouter"
In NextJS 9+, one way to ensure route parameters are immediately available for page components is to get them from the context arg passed to getServerSideProps() and pass to the component as props.
For a page like [id].js,
export function getServerSideProps(context) {
return {
props: {params: context.params}
};
}
export default ({params}) => {
const {id} = params;
return <div>You opened page with {id}</div>;
};
This is a great question and one that took a few days for me to figure out what the best approach is.
I have personally found three viable solutions to the problem of validating dynamic route path params or even just route path params in general.
The three solutions are
SSR (don't recommend) [Next >= 10]
useRouter
Middleware [Next 12 required]
In my examples a will use a route that requires a reset-token or it should be redirected.
SSR
Firstly server side rending with getServerSideProps.
Vercel recommends to use SSR as a last resort and I would highly recommend not using SSR when able (time to byte & cost).
We suggest trying Incremental Static Generation or Client-side Fetching and see if they fit your needs.
https://vercel.com/blog/nextjs-server-side-rendering-vs-static-generation
But in the case that you do, say there is some server side api validation call you require to validate the query param.
export const getServerSideProps = async (context) => {
const { token } = context.query;
if (!token) {
return {
redirect: {
permanent: false,
destination: "/",
}
}
}
return {
props: {}
// props: { token }
// You could do this either with useRouter or passing props
}
}
useRouter Secondly the easiest useRouter. When I first did this I came across the problem when nextjs/react hydrates there will be a point when the query params are null. Luckily useRouter has isReady!
import Router, { useRouter } from "next/router";
const { query, isReady } = useRouter();
useEffect(() => {
if (!isReady) return;
if (!query.token) {
Router.push("/")
}
}, [isReady])
Middleware now this is my personal favourite as it seperates the functionality in a clean way imo.
I found this based of a vercel example. I would highly recommend reading through a bunch of these to find best practices.
https://github.com/vercel/examples/
import { NextResponse, NextRequest } from 'next/server'
export async function middleware(req) {
const { pathname, searchParams } = req.nextUrl
if (pathname == '/reset-token') {
const index = searchParams.findIndex(x => x.key === "token")
// You could also add token validation here.
if (!index) {
return NextResponse.redirect('/')
}
}
return NextResponse.next()
}
Here is the repo which has some cool filtering of query parameters.
This is a more soft approach instead of hard redirecting.
https://github.com/vercel/examples/tree/main/edge-functions/query-params-filter
Nico also has a great answer on this, expect I wouldn't recommend using hooks like in his example, instead use isReady.
https://stackoverflow.com/a/58182678/4918639
For Class Component Lovers
The even better approach is to listen for a dedicated event for this routeChangeComplete using this.props.router.events.on method, inside componentDidMount if you're using class component -
routeChangeComplete = () => {
// this WILL have valid query data not empty {}
console.log(this.props.router.query);
};
componentDidMount() {
this.props.router.events.on("routeChangeComplete", this.routeChangeComplete);
}
componentWillUnmount() {
this.props.router.events.off("routeChangeComplete", this.routeChangeComplete);
}
Ref: https://nextjs.org/docs/api-reference/next/router#routerevents
routeChangeComplete: Fires when a route changed completely.
Practically when isReady has become true or when router.query object has data.
For NextJS version - 12.0.8
"If you export a function called getServerSideProps (Server-Side Rendering) from a page, Next.js will pre-render this page on each request using the data returned by getServerSideProps."
=async functions
refference:https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props#getserversideprops
Simply putting that async function on the page notifies NextJS of its presence.During prerendering stage of the component, the query object of the router will be empty.
isReady: boolean - Whether the router fields are updated client-side and ready for use. Should only be used inside of useEffect methods and not for conditionally rendering on the server.
refference: https://nextjs.org/docs/api-reference/next/router
solution:
import { useRouter } from 'next/router';
const Fn = () =>{
const router = useRouter();
const { param } = router.query;
const fetchData = async () => {
await fetch();
}
useEffect(() => {
fetchCat();
}, [router.isReady]);
}
I resolved my problem that I need it in Hoc component.
I wrapped using withRouter(withLocale(Comp)) and create conditional in HOC
export default function withLocale(WrappedPage) {
const WithLocale = ({ router, ...props }) => {
const { lang } = router.query;
if (!lang || !isLocale(lang)) {
return <Error statusCode={404} />;
}
return (
<LocaleProvider lang={lang}>
<WrappedPage {...props} />
</LocaleProvider>
);
};
return WithLocale;
}
Next.js <= 10.0.5
This is a good work around, I found around from this comment
Add useQuery.ts helper file
// useQuery.js
import { useRouter } from 'next/router';
// Resolves query or returns null
export default function useQuery() {
const router = useRouter();
const ready = router.asPath !== router.route;
if (!ready) return null;
return router.query;
}
usage
// In your components (instead of useRouter)
const query = useQuery();
useEffect(() => {
if (!query) {
return;
}
console.log('my query exists!!', query);
}, [query]);
Class Component | 12/16/2022 | React JS 18.2.0 | Next JS 13.0.6
I got the answer for those who want to use Class Component. This was actually nowhere to be found ! Enjoy !
You will add if(this.props.router.isReady) and include return in the condition in render().
.
.
import { withRouter } from 'next/router';
import { Component } from 'react';
class User extends Component {
...
render() {
if(this.props.router.isReady){ // Add this condition and include return ()
// Do anything
console.log(this.props.router.query) // Log 'query' on first render
return (
<div>
<SearchBar pid={this.props.router.query.pid} /> // Pass the query params to another component if needed
</div>
);
}
}
}
export default withRouter(User);
In my application on Next.Js i use redux and redux saga. I want to use ssr making http requests:
export const getStaticProps = wrapper.getStaticProps(async ({ store }) => {
store.dispatch(getDataRequest());
store.dispatch(END);
await store.sagaTask.toPromise();
});
In the same time i want to get data of the above result:
const selector = useSelector((s) => s);
console.log(selector);
The issue is that, when i run the application i get an error:
Error: could not find react-redux context value; please ensure the component is wrapped in a <Provider>
I used the provider but the data doesn't appear. Question: How to solve the issue in my application?
demo: https://codesandbox.io/s/awesome-butterfly-f7vgd?file=/pages/store/saga.js
this is your _app component:
function App({ Component, pageProps }) {
return (
<div>
<Provider store={makeStore}>
<Component {...pageProps} />
</Provider>
</div>
);
}
you dont need to wrap it with Provider. this is the only thing you need to do in _app.
export default wrapper.withRedux(App)
this is getStatisProps in your pages/index.js
export const getStaticProps = wrapper.getStaticProps(async ({ store }) => {
store.dispatch(getDataRequest());
store.dispatch(END);
await store.sagaTask.toPromise();
});
see store is already passed here. You will be accesing state via store.getState(). this is how it should be
export const getStaticProps = wrapper.getStaticProps(async ({ store }) => {
store.dispatch(getDataRequest());
store.dispatch(END);
await store.sagaTask.toPromise();
const state = store.getState();
return {props:{data:state}} // since you have only one reducer
});
now data will be passed as a prop to your component. if you console.log(props.data) inside the component, you should be seeing your dataReducer
I have an app that uses next.js along with Apollo/ Graphql and i'm trying to fully understand how the getInitialProps lifecycle hook works.
The lifecycle getInitialProps in my understanding is used to set some initial props that will render server side for when the app first loads which can be used prefetch data from a database in order to help SEO or simply to enhance page load time.
My question is this:
Every time I have a query component that fetches some data in my
components across my app, do I have to use getInitialProps to be
sure that data will be rendered server side?
My understanding is also that getInitialProps will only work in the page index components (as well as in _app.js), this would mean that any component lower down in the component tree would not have access to this lifecycle and would need to get some initial props from way up at the page level and then have them passed down the component tree. (would be great if someone could confirm this assumption)
Here is my code:
_app.js (in /pages folder)
import App, { Container } from 'next/app';
import { ApolloProvider } from 'react-apollo';
class AppComponent extends App {
static async getInitialProps({ Component, ctx }) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
// this exposes the query to the user
pageProps.query = ctx.query;
return { pageProps };
}
render() {
const { Component, apollo, pageProps } = this.props;
return (
<Container>
<ApolloProvider client={apollo}>
<Component client={client} {...pageProps} />
</ApolloProvider>
</Container>
);
}
}
export default AppComponent;
Index.js (in /pages/users folder)
import React, { PureComponent } from 'react';
import { Query } from 'react-apollo';
import gql from 'graphql-tag';
const USERS_QUERY = gql`
query USERS_QUERY {
users {
id
firstName
}
}
`;
class Index extends PureComponent {
render() {
return (
<Query query={USERS_QUERY}>
{({data}) => {
return data.map(user => <div>{user.firstName}</div>);
}}
</Query>
);
}
}
export default Index;
The answer is NO
If you use Apollo with Next JS you will not have to use getInitialProps on each page to get some initial data rendered server side. The following configuration for getInitialProps is enough for all the components to render out with their respective queries if they have <Query> components in them
static async getInitialProps({ Component, ctx }) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
// this exposes the query to the user
pageProps.query = ctx.query;
return { pageProps };
}
My issue and why I wasnt seeing any server side rendering is that Heroku or Now wouldnt perform SSR with a public URL ie my-app.heroku.com. To resolve this I purchased and applied a custom URL in Heroku and it worked. Along with a custom URL I had the following configuration in my Apollo config
const request = (operation) => {
operation.setContext({
fetchOptions: {
credentials: 'include'
},
headers: { cookie: headers.cookie }
});
};
This completely resolved it and now I have SSR without the pain of having to manually set getInitialProps on each page
Hope this helps someone
Bit of an odd situation here - I have a website written in Vue and I want to demo a library I've written in react. I can avoid server side rendering (SSR) by wrapping ReactDOM.hydrate(ReactApp, document.getElementById('react'area')) but I don't want to do that. I want to render everything SSR, but I don't see how it's possible.
Here is my renderOnServer.js for vue:
process.env.VUE_ENV = 'server'
const fs = require('fs')
const path = require('path')
const filePath = './App/dist/server.js'
const code = fs.readFileSync(filePath, 'utf8')
const vue_renderer = require('vue-server-renderer').createBundleRenderer(code)
//prevent XSS attack when initialize state
var serialize = require('serialize-javascript')
var prerendering = require('aspnet-prerendering')
module.exports = prerendering.createServerRenderer(function (params) {
return new Promise((resolve, reject) => {
const context = {
url: params.url,
absoluteUrl: params.absoluteUrl,
baseUrl: params.baseUrl,
data: params.data,
domainTasks: params.domainTasks,
location: params.location,
origin: params.origin,
xss: serialize("</script><script>alert('Possible XSS vulnerability from user input!')</script>")
}
const serverVueAppHtml = vue_renderer.renderToString(context, (err, _html) => {
if (err) { reject(err.message) }
resolve({
globals: {
html: _html,
__INITIAL_STATE__: context.state
}
})
})
})
});
So basically I'm configuring SSR above to read server.js:
import { app, router, store } from './app'
export default context => {
return new Promise((resolve, reject) => {
router.push(context.url)
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
if (!matchedComponents.length) {
return reject(new Error({ code: 404 }))
}
Promise.all(matchedComponents.map(Component => {
if (Component.asyncData) {
return Component.asyncData({ store, context })
}
}))
.then(() => {
context.state = store.state
resolve(app)
})
.catch(reject)
}, reject)
})
}
and server.js above is just looking for the right vue component and rendering. I have a test react component:
import React from 'react'
export default class ReactApp extends React.Component {
render() {
return (
<div>Hihi</div>
)
}
}
and my vue component:
<template>
<div id="page-container">
<div id="page-content">
<h3 class="doc-header">Demo</h3>
<div id="react-page">
</div>
</div>
</div>
</template>
<script>
<script>
import ReactApp from './ReactApp.jsx'
import ReactDOM from 'react-dom'
export default {
data() {
return {
}
},
}
ReactDOM.hydrate(ReactApp, document.getElementById('#react-page'))
</script>
But obviously it won't work because I can't use document in SSR.
Basically, the purpose of hydrate is to match react DOM rendered in browser to the one that came from the server and avoid extra render/refresh at load.
As it was pointed in the comments hydrate should be used on the client-side and React should be rendered on the server with renderToString.
For example, on the server it would look something like this:
const domRoot = (
<Provider store={store}>
<StaticRouter context={context}>
<AppRoot />
</StaticRouter>
</Provider>
)
const domString = renderToString(domRoot)
res.status(200).send(domString)
On the client:
<script>
const domRoot = document.getElementById('react-root')
const renderApp = () => {
hydrate(
<Provider store={store}>
<Router history={history}>
<AppRoot />
</Router>
</Provider>,
domRoot
)
}
renderApp()
</script>
Technically, you could render React components server-side and even pass its state to global JS variable so it is picked up by client React and hydrated properly. However, you will have to make a fully-featured react rendering SSR support(webpack, babel at minimum) and then dealing with any npm modules that are using window inside (this will break server unless workarounded).
SO... unless it is something that you can't live without, it is easier, cheaper and faster to just render React demo in the browser on top of returned Vue DOM. If not, roll up your sleeves :) I made a repo some time ago with react SSR support, it might give some light on how much extra it will be necessary to handle.
To sum everything up, IMO the best in this case would be to go with simple ReactDOM.render in Vue component and avoid React SSR rendering:
<script crossorigin src="https://unpkg.com/react#16/umd/react.production.min.js"></script>
<script src="my-compiled-react-bundle.js"></script>
<script>
function init() {
ReactDOM.render(ReactApp, document.getElementById('#react-page'))
}
init();
</script>
I am following this tutorial: https://crypt.codemancers.com/posts/2017-06-03-reactjs-server-side-rendering-with-router-v4-and-redux/ which i think is the 'standard' way of doing server side rendering in react (?).
Basically what happens is i use react router (v4) to make a tree of all the components that are about to get rendered:
const promises = branch.map(({ route }) => {
return route.component.fetchInitialData
? route.component.fetchInitialData(store.dispatch)
: Promise.resolve();
});
Wait for all those promises to resolve and then call renderToString.
In my components i have a static function called fetchInitialData which looks like this:
class Users extends React.Component {
static fetchInitialData(dispatch) {
return dispatch(getUsers());
}
componentDidMount() {
this.props.getUsers();
}
render() {
...
}
}
export default connect((state) => {
return { users: state.users };
}, (dispatch) => {
return bindActionCreators({ getUsers }, dispatch);
})(Users);
And all this works great except that getUsers is called both on the server and the client.
I could of course check if any users are loaded and not call getUsers in componentDidMount but there must be a better, explicit way to not make the async call twice.
After getting more and more familiar with react i feel fairly confident i have a solution.
I pass a browserContext object along all rendered routes, much like staticContext on the server. In the browserContext i set two values; isFirstRender and usingDevServer. isFirstRender is only true while the app is rendered for the first time and usingDevServer is only true when using the webpack-dev-server.
const store = createStore(reducers, initialReduxState, middleware);
The entry file for the browser side:
const browserContext = {
isFirstRender: true,
usingDevServer: !!process.env.USING_DEV_SERVER
};
const BrowserApp = () => {
return (
<Provider store={store}>
<BrowserRouter>
{renderRoutes(routes, { store, browserContext })}
</BrowserRouter>
</Provider>
);
};
hydrate(
<BrowserApp />,
document.getElementById('root')
);
browserContext.isFirstRender = false;
USING_DEV_SERVER is defined in the webpack config file using webpack.DefinePlugin
Then i wrote a HOC component that uses this information to fetch initial data only in situations where it is needed:
function wrapInitialDataComponent(Component) {
class InitialDatacomponent extends React.Component {
componentDidMount() {
const { store, browserContext, match } = this.props;
const fetchRequired = browserContext.usingDevServer || !browserContext.isFirstRender;
if (fetchRequired && Component.fetchInitialData) {
Component.fetchInitialData(store.dispatch, match);
}
}
render() {
return <Component {...this.props} />;
}
}
// Copy any static methods.
hoistNonReactStatics(InitialDatacomponent, Component);
// Set display name for debugging.
InitialDatacomponent.displayName = `InitialDatacomponent(${getDisplayName(Component)})`;
return InitialDatacomponent;
}
And then the last thing to do is wrap any components rendered with react router with this HOC component. I did this by simply iterating over the routes recursively:
function wrapRoutes(routes) {
routes.forEach((route) => {
route.component = wrapInitialDataComponent(route.component);
if (route.routes) {
wrapRoutes(route.routes);
}
});
}
const routes = [ ... ];
wrapRoutes(routes);
And that seems to do the trick :)