1

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.

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.