I have an existing application that was created with create-react-app and I would like to add Server Side Routing to this, primarily for the SEO benefits.
Following several tutorials, I have a basic working application, however I can't seem to get it to play nicely with react-router. I have one of two scenarios: either the routing works, but the functionality doesn't (ie. no event handlers, CSS is wonky, etc.) or the functionality is there, but the routing doesn't work (ie. only loads the default route /, all other routes result in a Cannot GET /route.
I have included the following files in the question:
server/index.js - This is the server that renders the app and serves it
webpack.server.js - Builds
.babelrc.json - Babel Configuration
src/index.tsx - The main client side entry point
src/App.tsx - The application itself
src/routes.tsx - The routes that I wish to use
Inside of server/index.js if I route to '*', then the site will route everything correctly. I will be able to route to /render/ or /display and I will see the appropriate pages, I won't however be able to actually click anything, because it seems as though it hasn't been properly hydrated. None of the data fetching works, and none of the click handlers do either. If I instead use '/', then the site will actually function, however I won't be able to route to other pages. Not even the 404 works.
Ultimately I would like to know how to best configure this to properly work. I have considered also switching to a framework like Next.js however this would be a significant undertaking, so I'm not sure that it would be worth it.
The jist of the application looks like this:
server/index.js
import React from 'react'
import ReactDOMServer from 'react-dom/server'
import express from 'express'
import { StaticRouter } from 'react-router-dom/server'
import path from 'path'
import fs from 'fs'
import App from '../src/App'
const PORT = process.env.PORT || 3006
const app = express()
app.get('/', (req, res) => { // <- Changing this between '*' and '/'
const context = {}
const app = ReactDOMServer.renderToString(
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
)
const indexFile = path.resolve('./build/index.html')
fs.readFile(indexFile, 'utf8', (err, data) => {
if (err) {
console.error('Something went wrong:', err)
return res.status(500).send('Oops, better luck next time!')
}
return res.send(
data.replace('<div id="root"></div>', `<div id="root">${app}</div>`)
)
})
})
app.use(express.static('./build'))
app.use(
'/',
(req, res, next) => {
console.log('Request URL:', req.originalUrl)
console.log('URL:', req.url)
next()
},
(req, res, next) => {
console.log('Request Type:', req.method)
next()
}
)
app.listen(PORT, () => {
console.log(`Server is listening on port ${PORT}`)
})
webpack.server.js
const path = require('path')
const nodeExternals = require('webpack-node-externals')
module.exports = {
entry: './server/index.js',
target: 'node',
externals: [nodeExternals()],
output: {
path: path.resolve('server-build'),
filename: 'index.js',
},
module: {
rules: [
{
test: /\.ts(x?)$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
},
{
loader: 'ts-loader',
},
],
},
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
},
],
},
],
},
resolve: {
extensions: ['.js', '.ts', '.tsx'],
},
}
.babelrc.json
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
}
src/index.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import { BrowserRouter } from 'react-router-dom'
ReactDOM.hydrate(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>,
document.getElementById('root') as HTMLElement
)
src/App.tsx
import React from 'react'
import { createTheme, ThemeProvider } from '@mui/material/styles'
import defaultTheme from './default'
import Router from './routes'
import { QueryClient, QueryClientProvider } from 'react-query'
import {
ExtendedStringifyOptions,
QueryParamProvider,
transformSearchStringJsonSafe,
} from 'use-query-params'
import { useNavigate, useLocation, Location } from 'react-router-dom'
const App = () => {
const theme = createTheme(defaultTheme)
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: false,
staleTime: 30000,
},
},
})
const queryStringifyOptions: ExtendedStringifyOptions = {
transformSearchString: transformSearchStringJsonSafe,
}
// Needed this in order to use useQueryParams with React Router v6
// From https://www.robinwieruch.de/react-router-search-params/
const RouteAdapter = ({ children }: any) => {
const navigate = useNavigate()
const location = useLocation()
const adaptedHistory = React.useMemo(
() => ({
replace(location: Location) {
navigate(location, { replace: true, state: location.state })
},
push(location: Location) {
navigate(location, { replace: false, state: location.state })
},
}),
[navigate]
)
return children({ history: adaptedHistory, location })
}
return (
<QueryParamProvider
stringifyOptions={queryStringifyOptions}
ReactRouterRoute={RouteAdapter}
>
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={theme}>
<Router />
</ThemeProvider>
</QueryClientProvider>
</QueryParamProvider>
)
}
export default App
src/routes.tsx
import React from 'react'
import { Routes, Route } from 'react-router-dom'
import DisplayView from './views/display-view'
import EmptyView from './views/empty-view'
import RenderView from './views/render-view'
const Router = (): JSX.Element => {
return (
<Routes>
<Route path={'/'} element={<RenderView />} />
<Route path={`/render`} element={<RenderView />} />
<Route path={`/display`} element={<DisplayView />} />
<Route path='*' element={<EmptyView />} />
</Routes>
)
}
export default Router
package.json
{
...
scripts: {
"dev:build-server": "cross-env NODE_ENV=development webpack --config webpack.server.js --mode=development -w",
"dev:start": "nodemon ./server-build/index.js",
"dev": "npm-run-all --parallel build dev:*"
}
...
}
Update: The order inside the server/index.js file seems to make a big difference. If I switch the order of the line: app.use(express.static('./build')) to come at the very beginning, then things seem to work themselves out, and the app will route properly, as well as function as it should.