How to setup api calls in MERN Stack app (Server Side Rendering)

Viewed 1710

I have created a react app that's a server side rendered, the problem am facing is setting up API calls. basically i want to get data from the database but i couldn't figure out how to do it properly.

As you may know in the static MERN App / REST API the server sends data according to the requested routes through the data property, then we get it in the client using res.data but in the server side rendered app i can't do that because the server always sends HTML tag of the page that we request to the client through res.data property which makes sense, but what are the correct approach of setting up routes in a way that we can get and post data to the database in server rendered app

Any help would really be appreciated, Thanks in advance

server/server.js

import React from "react";
import express from "express";
import compression from "compression";
import { Provider } from "react-redux";
import ReactDOMServer from "react-dom/server";
import { StaticRouter } from "react-router";
import store from "../client/store/store";
import App from "../client/App";
import mongoose from "mongoose";
import path from "path";
import fs from "fs";
import cors from "cors";
import User from "./model/users";

const app = express();
const PORT = process.env.PORT || 3000;

app.use(cors());
app.use(compression());
app.use(express.json());
app.use(express.static("./build/public"));

mongoose
  .connect(
    process.env.MONGODB_URI,
    {
      useNewUrlParser: true,
      useCreateIndex: true,
      useUnifiedTopology: true
    }
  )
  .then(() => {
    console.log("Connected to the Database...");
  })
  .catch(err => console.log("ERROR: " + err));

app.get("*", (req, res) => {
  const content = ReactDOMServer.renderToString(
    <Provider store={store}>
      <StaticRouter location={req.url} context={{}}>
        <App />
      </StaticRouter>
    </Provider>
  );

  const indexFile = path.resolve("./build/public/index.html");
  fs.readFile(indexFile, "utf8", (err, data) => {
    if (err) {
      console.log(err);
      return res.status(500).send("Oops, Something went wrong!");
    }

    return res.send(
      data.replace(
        '<div id="root"></div>',
        `<!DOCTYPE html>
        <html lang="en">
          <head>
            <meta charset="UTF-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1.0" />
            <meta http-equiv="X-UA-Compatible" content="ie=edge" />
            <title>My App</title>
          </head>
          <body>
            <div id="root">${content}</div>
          </body>
        </html>`
      )
    );
  });
});

// app.get("/api/getData", (req, res) => {
//   User.find({}, (err, users) => {
//     if (err) throw err;
//     res.json({ newUser: users });
//   });
// });

app.listen(PORT, () => {
  console.log("server is listening to port:", PORT);
});

client/client.js

import "./assets/scss/styles.scss";
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
import store from "./store/store";

function importAll(r) {
  let images = {};
  r.keys().map(item => {
    images[item.replace("./", "")] = r(item);
  });
  return images;
}
importAll(require.context("./assets/images", false, /\.(png|jpe?g|svg)$/));

ReactDOM.hydrate(
  <Provider store={store}>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </Provider>,
  document.querySelector("#root")
);

client/App.js

import React, { Component } from "react";
import { connect } from "react-redux";
import { Route, Switch } from "react-router-dom";
import Home from "./pages/Home";
import About from "./pages/About";
import Themes from "./pages/themes";
import Contact from "./pages/Contact";
import ErrorBoundary from "./pages/ErrorBoundary";

class App extends Component {
  render() {
    return (
      <ErrorBoundary>
        <Switch>
          <Route exact path="/" component={Home} />
          <Route exact path="/about" component={About} />
          <Route exact path="/themes" component={Themes} />
          <Route exact path="/contact" component={Contact} />
        </Switch>
      </ErrorBoundary>
    );
  }
}

export default connect()(App);

client/pages/Home.js

import React, { Component } from "react";
import { connect } from "react-redux";
import axios from "axios";

class Home extends Component {
  componentDidMount() {
    axios
      .get("api/getData")
      .then(res => {
        if (res) {
          console.log(res.data);
        }
      })
      .catch(err => console.log(err));
  }

  render() {
    return (
      <div><h1>Welcome Home</h1></div>
    );
  }
}

export default connect()(Home);
1 Answers

There are 3 approaches to follow (with sub-cases for one of them):

  1. Have different routes for outputing HTML(rendered on server) and JSON data (to be rendered on client). Simple and clean but verbose.

  2. a. Output html as usual but check if request is made through XmlHttpRequest/XHR and then output JSON data only.

  3. b. Output html as usual but have client-side code add a special parameter when needs to get simply JSON data back. Then on server check if this parameter is present. eg ?output=json.

  4. Send JSON data from db along with html output. (This is an option I am not very sure, so I leave it as an idea)

Here are some resources to get you ideas on react server-side rendering:

  1. Server Rendering with React and React Router
  2. Server Rendering, React Training
  3. Using React Router 4 with Server-Side Rendering
  4. react-data-fetching-components, Asynchronously load data for your React components with SSR (github)
Related