react-router v4 with Webpacker & SSR
Steps to reproduce
React v5 with latest version of react-rails and using webpacker to compile assets (loaded through javascript_pack_tag and react_component)
Install react-router v4: yarn add react-router
Attempt to add some dynamic or static routes using <BrowserRouter> or <StaticRouter>.
Expected behavior
Be able to use <BrowserRouter> and/or <StaticRouter>. I know there is some documentation in the Wiki but it's related to v3 of the router and it's using sprockets.
Actual behavior
For <BrowserRouter>:
Encountered error "#<ExecJS::ProgramError: Invariant Violation: Browser history needs a DOM>" when prerendering REDACTED with {"title":"This is the title","body":"This is the body"}
invariant (webpack-internal:///3:40:15)
createBrowserHistory (webpack-internal:///62:49:27)
new BrowserRouter (webpack-internal:///60:38:231)
resolve (webpack-internal:///48:2085:14)
ReactDOMServerRenderer.render (webpack-internal:///48:2242:22)
ReactDOMServerRenderer.read (webpack-internal:///48:2216:19)
Object.renderToString (webpack-internal:///48:2483:25)
Object.serverRender (webpack-internal:///33:70:42)
eval (eval at <anonymous> ((execjs):1155:8), <anonymous>:6:45)
eval (eval at <anonymous> ((execjs):1155:8), <anonymous>:18:13)
For <StaticRouter>: the links either don't work or Rails is trying to get to the linked URL and fail to load?
System configuration
Webpacker version: 3.0 React-Rails version: 2.4 Rect_UJS version: 2.4 Rails version: 5.1.4 Ruby version: 2.4.2
Trying to integrate react-routes v4 with a Webpacker setup. Can't seem to find any documentation on how to do it with v4 and it doesn't seem to work. If anyone got them to work, some guidance would me much appreciated.
Hi @RazvanDH
Thanks for getting in touch!
I've not used react-router before but I'd imagine that BrowserRouter just from name won't be able to be used server-side as it will interact with DOM, hence your error message.
What error do you get with the StaticRouter?
Here is a doc provided by react-router themselves for server-side rendering. Could you let me know how it goes? https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/guides/server-rendering.md
From a skim seems like you will need to write your components in such a way as BrowserRouter never triggers server-side using the react lifecycle methods.
I was having similar problems. What you need to do is make sure that your app renders with a <StaticRouter> when prerendering and a <BrowserRouter> when running in a web browser. Here's my solution:
App view
<%= react_component('Application', {path: request.path}, {prerender: true}) %>
App root component
import { BrowserRouter, StaticRouter } from 'react-router-dom'
class Router extends React.Component {
renderRouter = () => {
if(typeof window !== 'undefined') {
return(
<BrowserRouter>
{this.props.children}
</BrowserRouter>
)
} else {
return(
<StaticRouter location={this.props.path} context={{}}>
{this.props.children}
</StaticRouter>
)
}
}
render() {
return(this.renderRouter())
}
}
const Application = props => (
<Router path={props.path}>
<h1>Hello, world!</h1>
</Router>
)
export default Application
Side note: React Router's documentation misleadingly implies that you should pass in the URL into <StaticRotuer>'s location prop, whereas you actually need to pass in the path. (I banged my head against that one for a while.)
That looks interesting, but how do you get the Rails data from INSIDE server-rendered js? When I used client-rendered SPA I used the code like that:
componentDidMount() {
axios.get('/companies.json').then(response => {
store.dispatch({
type: 'COMPANIES_LIST',
companies: response.data
})
})
}
But such code does not work in server-side js. But what instead?
@programrails pass it in as props server side, that can be your components default state, then save axios for when something in the system changes rather than the componentDidMount.
I think we can close this issue. @BookOfGreg and @ersinakinci have suggested a great solution.