Fetching API Data with Web Components and SSR
Tags: webdev, webcomponents, javascript, node
Published: 2020-05-12 16:50:44

I recently launched a site built entirely with Web Components. Several views where static, meaning I could just server side render them wholesale without must hassle. When it came to the blog I felt stuck. The blog is dependent on the asynchronous response from a REST API. How was I going to server side render this view?

The solution turned out to be easy!

![](https://media.giphy.com/media/41xF6qBDznhIY/giphy.gif)

The `BlogComponent` shown in the code snippet below is a custom element that represents the view. During the `connectedCallback` the method `getModel` is called.

```javascript
class BlogComponent extends HTMLElement {
...
connectedCallback() {
    this.getModel();
}
getModel() {
  return new Promise((res, rej) => {
    fetch('http://localhost:4444/api/blog')
      .then(data => data.json())
      .then((json) => {
        this.renderPosts(json);
        res();
      })
      .catch((error) => rej(error));
  })
}
renderPosts(data) {
   ...code for rendering DOM elements
   ...lots of createElement, appendChild, yada yada
}
```

The `getModel` method returns a Promise so when the component is server side rendered the Express middleware can wait for `getModel` to finish before responding with the rendered view. The important thing to consider here is when server side rendering Express will need to wait for the HTTP request to finish before responding to a client with the rendered HTML for the page. 

In the Promise, I used `fetch` to make the HTTP request to `/api/blog`. I call `res()` after the component renders the view using the data from the JSON response. In this example, `renderPosts` is a blocking function. You may use any pattern you see fit. Perhaps you want to implement MVC or service pattern for your components. Go ahead! There just needs to be a way at the component level server side rendering can analyze the class and determine "Does this component require data before I render it?"

During a build step this component is bundled with every other component and route configuration for server side rendering. I map a reference to each component to the path the user will visit, along with a title that can be viewed in the browser window.

```javascript
export const routes = [
  { path: '/', component: HomeComponent, title: 'Home' },
  { path: '/blog', component: BlogComponent, title: 'Blog' },
  { path: '/404', component: FileNotFoundComponent, title: 'File Not Found' }
]
```

The bundle is imported into Express middleware that handles the server side rendering. For server side rendering custom elements I used the `@skatejs/ssr` package. The middleware is below.

```javascript
require('@skatejs/ssr/register');
const render = require('@skatejs/ssr');

const url = require('url');
const path = require('path');
const fs = require('fs');
const { routes } = require('path/to/bundle.js');

const indexPath = path.resolve('path/to/index.html');
const dom = fs.readFileSync(indexPath).toString();

function generateHTML(template, route, dom){
  return dom
  .replace(`<title></title>`, `<title>${route.title}</title>`)
  .replace(`<div id="root"></div>`, `<div id="root">${template}</div>`)
  .replace(/__ssr\(\)/g, '')
}

export default async(req, res, next) => {
  let component = {};
  const route = routes.find(rt => rt.path === url.parse(req.url).pathname);
  if (route == undefined) {
    res.redirect(301, '/404');
    return;
  } else {
    component = route.component;
  }
  if (component) {
    const preRender = new component();
    if (preRender.getModel) {
      try {
        await preRender.getModel();
      } catch(e) {
        next(e);
      }
    }
    const template = await render(preRender);
    res.send(generateIndex(template, route, dom));
  } else {
    res.send(dom);
  }
}
```

async/await made this code somewhat compact. After the middleware establishes a component is mapped to this route by parsing the request url and checking against a route in the imported config, the component is instantiated. If a route doesn't match, the browser will be redirected to a `404` route.

```javascript
const route = routes.find(rt => rt.path === url.parse(req.url).pathname);
if (route == undefined) {
    res.redirect(301, '/404');
    return;
} else {
    component = route.component;
}
if (component) {
    const preRender = new component();
```

If a class passes through this middleware has the `getModel` method, `getModel` is called using the `await` keyword. The `getModel` method returns a Promise that ensures the component has rendered the template after successfully making the HTTP request. The code is wrapped in a try / catch in case something fails (either the HTTP request or the render method in the component). 


```javascript
if (preRender.getModel) {
  try {
    await preRender.getModel();
  } 
  catch(e) {
    next(e);
  }
}
```

But wait, you are calling `fetch` from node, but inside code that is normally client side?

![](https://media.giphy.com/media/Zba7dZIYpdwkM90Oag/giphy.gif)

Remember how I said the components are bundled specifically for server side rendering in a build step? In the entry point for that bundle, I imported `node-fetch` and put it on the global namespace.

```javascript
import fetch from 'node-fetch';
global['fetch'] = fetch;
```

I put objects on the global namespace here normally when I want to mock browser based APIs for server side rendering. `node-fetch` is an implementation of the fetch API that allows the component to make the HTTP request in node.

The next `await` that follows is `@skatejs/ssr` method for rendering Web Components server side. `@skatejs/ssr` is the magic sauce. The `render` method takes the component in the first argument and returns the HTML. This HTML will be in the response from the server, but first I have to inject the component HTML into the DOM.

```javascript
const template = await render(preRender);
res.send(generateIndex(template, route, dom));
```
The `generateIndex` method takes the HTML that was retriever earlier in the code from a static file and places the server side rendered HTML in the `#root` element. I had to massage the output of the `render` method a bit and remove any calls to a `__ssr` function that it was injecting for `@skatejs` formatted components. The `generateHTML` method also sets the content of the `title` tag for SEO purposes.

```javascript
function generateHTML(template, route, dom){
  return dom
  .replace(`<title></title>`, `<title>${route.title}</title>`)
  .replace(`<div id="root"></div>`, `<div id="root">${template}</div>`)
  .replace(/__ssr\(\)/g, '')
}
```

The results are impressive. Express responds with the server side rendered blog posts on the initial request from the browser.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/dhnelbvx33jdh6ls6g1h.png)

## Easy Rant

A few months ago I read somewhere it can't be done. "You can't server side render web components" they said, justifying reasoning for sticking with React rather than adopting custom elements v1. [One of my latest projects](https://dev.to/steveblue/coding-my-personal-site-with-web-components-4941) demonstrates you can not only SSR static pages with custom elements, but also components that are dependent on asynchronous data from a REST API.

You can do anything with custom elements you can do with component based JavaScript libraries and frameworks, with less code and possibly more performant than Virtual DOM. It really comes down to implementation. In this example I demoed a simple implementation for server side rendering a web component using a class method. You're free to use any pattern. Free as a bird. Don't you want to be a bird?

![](https://media.giphy.com/media/2WYvNJqL8yXLy/giphy.gif)