Adding React to a Node Application with Jade or Pug
It was very easy to miss React after working with it for the last few years when we had to go back to an older code base of a product still being used and add some new features. The code base in question is a Node JS application that does server-side rendering using Jade (not even Pug).
After performing the customary Google searches, “add react to node jade pug”, “react server side rendering”, etc — it became pretty obvious that most out there had written about adding React to their code base without really explaining why anything had to be done the way they did it. This made it difficult to adapt their instructions to the particular situation we had.
I’ll explain the architecture we were working within, the questions we had to answer, and how we answered those questions in order to get React working within our application.
The first question was “how are we going to include React and ReactDOM?”. There are a couple of different options, such as including an external source like
<script src="https://unpkg.com/react@16/umd/react.production.min.js" crossorigin></script>
Our application was already using webpack to build a transpiled JavaScript bundle so the answer to the first question seemed pretty simple, we’d just npm install
all the things. We’d also need the react preset for babel because we were going to be writing jsx
and need it to be transpiled into our bundle.
npm install react react-dom @babel/preset-react
We know that the HTML was being rendered on the server by some Node JS controllers and Jade templates, and we needed to understand how the client-side JavaScript was being executed because in order to use React we’d need to use ReactDOM.render
. In our particular situation, we had an entry in our webpack.js
defined as
entry: { main: [path.resolve(SCRIPT_DIR, 'client.js')] }
Good or bad, client.js
was defining a global object that was being used to choose which JavaScript to associate to each Jade Template which looks like something like this
import Index from './index'
const client = {
Index: Index
}
global.Client = Client
Then in the Jade Template
block footer-scripts
script.
Client.Index.load()
What this told us was that we could put a div in the Jade Template where we wanted our React component and render it similar to how our other JavaScript was being loaded.
First we created a simple React component to test this. We called it test.jsx
with the intention of transpiling .jsx
files separately from our other .js
files to avoid a bunch of refactoring. We also knew that we didn’t want to actually try to render this component until the template was created, so we wrapped ReactDOM.render
into a function that we could call similarly to Client.Index.load()
. We decided it should be passed the id of the root div in order to avoid coupling of the React Component and the Jade Template
import React from 'react'
import ReactDOM from 'react-dom'class Test extends React.Component {
render() {
return (<div>Hello React</div>)
}
}function renderTest(rootDiv) {
ReactDOM.render(<Test/>, document.getElementById(rootDiv))
}export default renderTest
Once we have the React Component we want to render we need to place a div in the Jade Template, expose our render function, call our render function, and make sure we update our webpack config to support this new file.
Adding the div in the Jade template was simple. We just put it in the body where we knew we’d be able to verify the final result
.div(id='testRoot')
Then updated client.js
to expose the renderTest
function we wanted
import Index from './index'
import renderTest from './components/test.jsx'const client = {
Index: Index,
renderTest: renderTest
}
global.Client = Client
and called it the same way we were calling Index.load
in our Jade Template
block footer-scripts
script.
Client.Index.load()
script.
Client.renderTest('testRoot')
Finally, we updated our webpack.js
to transpile the jsx
we had written. We separated this test to handle .jsx
separately from our existing JavaScript
module: {
rules: [
{
test: /.jsx$/,
include: [SCRIPT_DIR],
exclude: /(node_modules)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/react']
}
}
}
]
}
And that was it! Good luck!