1

I have a 1-year React application that uses Server-Side Rendering and now we're developing a page that will be indexed by Googlebot.

The problem is: we need the response of an async api call to render a page within that data for SEO purposes. Googlebot (view-page-source) must not have a Loading... component or anything else. Its content MUST be that api data.

However all the examples/solutions that I found about it told to use componentDidMount, componentWillMount (deprecated) or even constructor, but all of them renders the page without the data first and Googlebot won't wait for it to finish.


Example:

API response:

{ trip: { description: 'It was great', title: 'The Trip! 2.0' } } 

The component:


class HomePage extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      data: null,
    }
  }

  render() {
    return (
      <div>
        {
          this.state.data ? <h1>{this.state.data.title}</h1> : <p>none</p>
        }
      </div>
    );
  }
};

export default HomePage;

Desired source code on all renders:

<h1>The Trip! 2.0</h1>

NEVER: <p>none</p>


PS: the api call is not being simulated on this example cause idk where to put it =x

What could I do to solve this problem considering that the component must NOT render without the api response? Is that possible? Thank you all!

  • Not sure I understand from the code you're showing: either the parent is doing the API call, and they can use `this.setSate`, with that state being referenced in their `render()` to set the property for this child (which should then use `this.props.blah` to access it), or this component itself runs the API call, and it calls `this.setState` as part of the call resolution, then references the state value you're updating in `render`. – Mike 'Pomax' Kamermans Feb 06 '20 at 18:32
  • Hi Mike! Thanks for your help but as I said at the end of the question this example does not simulate the api call because none of the cases I found on my search works the way I wanted. That's not a simple solution as it appears to be =/ – Renan Martins Feb 06 '20 at 18:38
  • And I didn't ask about that. Either your component shouldn't show until your API call resolves, in which case "whichever component _builds_ this one" should simply not even try to render this one until the API call resolves and the required data is available. E.g. `class Parent extends Component { constructor(props) { super(props); ...API call here, ending in a this.setState on completion } render() { return { this.state.data ? : null }; }` – Mike 'Pomax' Kamermans Feb 06 '20 at 18:41
  • That said: your homepage should always show something. There are too many non-edge-case situations in which your API call will not succeed client-side, and you should not show users an empty page in that case. – Mike 'Pomax' Kamermans Feb 06 '20 at 18:43
  • Thanks again Mike, but it looks like if I make the request on the client the robot will not index the content of the page. Actually for this page I want the page to load slower in order to wait for the must-have data. I will try some examples doing the request on the server side. – Renan Martins Feb 06 '20 at 19:48
  • No, obviously not. Robots index what they get on first page load. If you need your server to _output_ preformed HTML with that API request already worked in, you really need to update your post to talk about that use case. – Mike 'Pomax' Kamermans Feb 06 '20 at 19:50
  • r u using redux? – Yilmaz Feb 09 '20 at 15:24

1 Answers1

0

Inside of each component you have to define a function, let's name it as loadData. this function will do async work of your component. when your server gets a request, you have to look at that Url and then you have to decide which components to render.

For each component that needs to be rendered, we will call that loadData function that is attached to each of components to initiate the data loading process. The key here is that we are not doing some initial render of the application. We just have a set of components. Each one says “here is a resource that I need”. Then whenever someone makes a request, we look at the set of components that we should need to render to get the page to show up. Then we will take all those components, we will take these little data loading requirement functions that are attached to them and we will call each of those. All these dataLoad functions return promise, so we have to detect all of them resolved before we render our app. Let's say we have Users.js so we define our function as follow:

const loadData = store => {
  return store.dispatch(fetchUsers());
};

when we import our component, we import inside an object.

export default {
  loadData:loadData,
  component: connect(mapStateToProps, { fetchUsers })(UsersList)
};

We have to set up our Routes.js file with the help of react-router-config.

Routes.js

import React from "react";
import Home from "./pages/Home";
import Users from "./pages/UsersList";
import App from "./App";
import NotFoundPage from "./pages/NotFoundPage";

export default [
  {
    ...App,
    //App component will be shown inside each component.
    //array of routes will be used inside the App.js down below
    routes: [
      {
        path: "/",
        ...Home,
        exact: true
      },
      { ...UsersList, path: "/users" },
      { ...AdminsListPage, path: "/admins" },
      { ...NotFoundPage }
    ]
  }
];

Here is the App.js

import React from "react";
import Header from "./components/Header";
import { renderRoutes } from "react-router-config";
import { fetchCurrentUser } from "./actions";

//this component is to render components that each component uses in common
//props.route is the child components that are passed from Routes.js
const App = ({ route }) => {
  return (
    <div>
      <Header />
      {renderRoutes(route.routes)}
    </div>
  );
};

export default {
  component: App,
  //this function is get called by redux so store is passed in
  //as you might know Header should always know about authentication status
  //we populate the store with the authentication info so Header can use it.
  loadData: ({ dispatch }) => dispatch(fetchCurrentUser())
};

now we set up all the loadData functions, now it is time to invoke them when our server gets request. for this we will be using matchRoutes function from react-router-config.

app.get("*", (req, res) => {
  const store = createStore(req);

//express does not touch routing. it delegates everything to react.
//I will just place the logic behind invoking loadData functions here.
//matchRoutes will look at the path and will return the array of components to be loaded. 
//this is what matchRoutes function show `{ route: { loadData: [Function: loadData], path: '/users', component: [Object] },//component that we show
match: { path: '/users', url: '/users', isExact: true, params: {} } }
`
//
const promises = matchRoutes(Routes, req.path)
    .map(({ route }) => {
      return route.loadData ? route.loadData(store) : null;
    }) //we got the array of loadData functions now we are invoking them
    .map(promise => {
      if (promise) {
        return new Promise((resolve, reject) => {
          promise.then(resolve).catch(resolve);
        });
      }
    });
//Promise.all takes an array of promises and resolves when all of the items resolve
Promise.all(promises).then(()=>{
//here is when it is time to render your server-side code.

}).catch(e => console.log(e.message));


}
Yilmaz
  • 35,338
  • 10
  • 157
  • 202