• Login
  • Apply
Back to Blog

Building an SSR React Application with GraphQL & Deno

Unless you have been living under a rock, I am sure you have been hearing all the hype on Deno and/or GraphQL in the past few years. If you are intrigued by these technologies and enjoy learning new technologies by building with them, then you're in the right place!

 

BACKGROUND ON DENO

On May 13, 2018, Ryan Dahl, the creator of Node.js announced Deno, a new secure runtime for JavaScript and TypeScript, during his “10 Things I Regret About Node.js” talk. In this talk, he mentioned some of the improvements that Deno aims to implement, such as abstracting the need for dependency solutions like NPM modules and package.json, supporting TypeScript out of the box by implementing a snapshotted TypeScript compiler, and breaking the sandboxed environment of the V8 engine.

After two years, on May 13, 2020, it was announced that Deno was ready for production, and has been backed by major cloud providers such as AWS Lambda, Azure Functions, and Google Cloud Run.

 

OVERVIEW

Throughout this article, we will be building a simple application to show Rick and Morty character information utilizing React, GraphQL, and Deno. During the process, we will be implementing server-side rendering with Deno’s Oak framework, as well as executing and caching GraphQL queries with Deno’s Obsidian library.

 

LET'S GET RIGHT INTO IT!

First things first, we need to set up our file structure.

my-app

├── client

│   ├── Components

│   ├── static

│   ├── app.tsx

│   └── client.tsx

├── server.tsx

├── deps.ts

├── serverDeps.tsx

├── staticFileMiddleware.ts

├── tsconfig.json

└── README.md

 

IMPORTING DENO MODULES

Since we have determined all of the libraries that we will be importing on our client-side and server-side, we will begin by creating deps.ts files to import them into our application.

import React from 'https://dev.jspm.io/react@16.13.1';

import ReactDom from 'https://dev.jspm.io/react-dom@16.13.1';

import {

 ObsidianWrapper,

 useObsidian,

} from 'https://deno.land/x/obsidian@v0.1.6/ObsidianWrapper/ObsidianWrapper.jsx';

 

export { React, ReactDom, ObsidianWrapper, useObsidian };



import {

 Application,

 Router,

 Context,

 send,

} from 'https://deno.land/x/oak@v6.0.1/mod.ts';

import ReactDomServer from 'https://dev.jspm.io/react-dom@16.13.1/server';

export { Application, Router, ReactDomServer, Context, send };


SERVER-SIDE RENDERING

Now that we have imported all our modules in one central location, we can begin setting up our server. We will be utilizing Deno’s Oak middleware framework, and ReactDOMServer to render components to static markup.

import { Application, Router, ReactDomServer } from './serverDeps.ts';

import { React } from './deps.ts';

import App from './client/app.tsx';

import { staticFileMiddleware } from './staticFileMiddleware.ts';

 

const PORT = 3000;

 

// Create a new server

const app = new Application();

 

// Router for base path

const router = new Router();

 

router.get('/', handlePage);

 

// Bundle the client-side code

const [_, clientJS] = await Deno.bundle('./client/client.tsx');

 

// Router for bundle

const serverrouter = new Router();

serverrouter.get('/static/client.js', (context) => {

 context.response.headers.set('Content-Type', 'text/html');

 context.response.body = clientJS;

});

 

// Implement the routes on the server

app.use(staticFileMiddleware);

app.use(router.routes());

app.use(serverrouter.routes());

app.use(router.allowedMethods());

 

app.addEventListener('listen', () => {

 console.log(`Listening at http://localhost:${PORT}`);

});

await app.listen({ port: PORT });

 

// Function to render entire application as a string

function handlePage(ctx: any) {

 try {

   const body = (ReactDomServer as any).renderToString(<App />);

   ctx.response.body = `<!DOCTYPE html>

 <html lang="en">

   <head>

   <meta charset="UTF-8">

   <link rel="stylesheet" href="/static/style.css">

   <meta name="viewport" content="width=device-width, initial-scale=1.0">   

   <title>Rick and Morty</title>

   </head>

 <body >

   <div id="root">${body}</div>

 

   <script  src="/static/client.js" defer></script>

 </body>

 </html>`;

 } catch (error) {

   console.error(error);

 }

}

 

If you are familiar with Express.js and/or Koa.js the structure for creating routers and endpoints with the Oak framework above should be very familiar. If you skim through the code there are two main things that differ from what you may be used to: the Deno.bundle function on line 17 and the HTML file at the bottom of the code block.

The Deno.bundle function is one of the great features that Deno provides. Similar to how you would use a bundler like Webpack to transpile and compile your application in Node.js, Deno has that functionality out of the box. The Deno.bundle function takes an entry point of the client-side code as an argument and transpiles and bundles the client-side file structure into a single javascript file to be sent to the client.

The second nuance above is the handlePage function, starting on line 38. When this function is invoked, it begins by utilizing the renderToString function that is provided by the ReactDOMServer package. The React application is passed into this function and converts the entire application into an HTML string. So when a “GET” request for the root endpoint is received by the server, the entire React application is sent to the client as a string.

While the page does load faster, it is a marked down version of the page, meaning that it does not have any of the React functionality — this is why a request is made for the bundled javascript file on line 52. With server-side rendering, the React application never uses the render method and instead uses a hydrate method. Since the webpage is already rendered, the browser receives the bundled javascript file and parses through the rendered application, seamlessly reinjecting the React functionality into the application.

 

CLIENT-SIDE CODE & GRAPHQL

Now that we have covered server-side rendering from the server-side, let's take a look at how we are able to achieve this functionality on the client-side.

import { React, ReactDom } from '../deps.ts';

import App from './app.tsx';

 

// Hydrate the app and reconnect React functionality

(ReactDom as any).hydrate(<App />, document.getElementById('root'));



The code snippet above demonstrates the entry point into the client-side code. Notice how the hydrate method is used instead of render.

Pretty simple, right? Well, let’s look into how we can incorporate GraphQL fetching and caching functionality into our application.

import { React, ObsidianWrapper } from '../deps.ts';

 

import MainComponent from './Components/MainComponent.tsx';

 

declare global {

 namespace JSX {

   interface IntrinsicElements {

     div: any;

   }

 }

}

 

const App = () => {

 return (

   <ObsidianWrapper>

     <MainComponent />

   </ObsidianWrapper>

 );

};

 

export default App;

 

FETCHING & CACHING GRAPHQL REQUESTS

Finally, we will be taking a look at how we will be fetching and caching our GraphQL requests in the following code block.

import React from 'https://dev.jspm.io/react@16.13.1';

import Sidebar from './Sidebar.tsx';

import Carousel from './Carousel.tsx';

import { useObsidian } from '../../deps.ts';

 

declare global {

 namespace JSX {

   interface IntrinsicElements {

     h1: any;

     div: any;

   }

 }

}

 

const GET_CHARACTERS = `

query{

 characters(page:1){

  results

  {

    id

    name

    image

  }

}

}

`;

 

const MainPage = () => {

 const [info, setInfo] = (React as any).useState({});

 const [characters, setCharacters] = (React as any).useState([]);

 

 const { gather } = useObsidian();

 

 (React as any).useEffect(() => {

   gather(GET_CHARACTERS, {

     endpoint: 'https://rickandmortyapi.com/graphql',

     destructure: false,

   }).then((resp: any) => setCharacters([...resp.data.characters.results]));

 }, []);

 

 return (

   <div id='main-container'>

     <h1>Rick and Morty Character Guide</h1>

     <div id='app-container'>

       <Sidebar info={info} />

       <Carousel characters={characters} setInfo={setInfo} />

     </div>

   </div>

 );

};

 

export default MainPage;

We will first begin by importing the useObsidian hook which allows us to access the functionality provided by the wrapper. On line 31 we invoke the hook which gives us access to a few different methods. Here we are using the gather method which gives us the ability to fetch and cache GraphQL requests. The first parameter of this method is the GraphQL query and the second is an “options” parameter. For the sake of simplicity, we are using an external API and specifying to not destructure the query and response since this requires us to provide the application with the GraphQL schema. If you are curious about Obsidian’s normalization and destructuring capabilities, check out the article below.

Introducing Obsidian: GraphQL, built for Deno

 

WRAPPING UP

Now that we have built our core structure, we can add some additional functionality to query for more specific query information about each individual character. Finally, we add some minor styling to wrap it all up!

If you would like to have a look at the source code you can find the respository at GitHub.

 

CONCLUSION

And there you have it! As you can see there are some major changes when working with Deno as opposed to Node.js — we do not store external modules locally, TypeScript can be compiled out of the box, and we did not need to configure a webpack.config.js file to specify the compiling as Deno can natively perform all of the transpiling and bundling.

If you have any questions about any of this material covered feel free to post a comment below or reach out via LinkedIn.

Cheers!

Blog written by Alonso G., Codesmith NY Cohort 20

---

Alonso is one of many graduates sharing their insights on the Codesmith Immersive experience. Take a look at this blog where one of our recent grads Rachel shares her path to Codesmith and helpful insights into our prep programs.