React, GraphQL, and Rails

May 24, 2020

Record Temps

Recently, I have been working on a new application that allows Instagram content creators to have a custom webpage-like experience in their “link in bios”, called Record Temps. In short, the technical requirements include a web application that can interface with the Instagram API (which FB makes really hard!) to pull posts and an admin UX to create their link in bio pages.

There are probably a million ways to tackle an application like this, but I had a few priorities in mind that an engineer may not have considered as “ideal”. It ended up resulting in a pretty interesting stack with a few pros and cons.

Goals

  • The primary goal of this was to create something using GraphQL instead of using HTTP requests. Having worked in React for years, this was something that was always top of mind, but never found a project right for it.
  • Next, was to further strengthen my skills at using Ruby on Rails. Having recently started a job at Betterment that is Rails-heavy, I wanted to prioritize being effective. For me, this meant getting to up to speed quickly with not only the Ruby language, but also ActiveRecord, RSpec, and Rails MVC as a whole.
  • Lastly, development speed (but not application speed) was a priority. Having exposure to the vast library of gems, and other first-class Ruby integrations (like Stripe) would be nice to speed things up. Mundane tasks like user authentication/registration would be much faster in a rolled framework. Rails 6 switched all to the way to webpacker, too, making Typescript/JS integration that much better.

Stack

With those goals in mind, the stack that I went with ended up being:

  • Ruby on Rails
  • Typescript
  • React
  • Apollo for GraphQL
  • SCSS/Bootstrap (instead of something like CSS Modules or emotion)

Pros

ActiveRecord

Coming from the Node world, one of my biggest frustrations is the lack of a mature ORM. Rails, with ActiveRecord, is a completely different story that provides excellent interface to modeling and querying. For Record Temps, this meant having well tested, flexible models that made development fast without too much planning ahead of time.

Other examples include:

  • User management would be basically free, with password hashing/other best practices
  • Mature querying language that is easy to understand and powerful features such as scopes
  • Extensible, even without database models. I ended up creating a custom Instagram API with include ActiveModel that gave me all the benefits of modeling without interfacing with the database at all!

Using graphql-ruby

While Rails is known as the MVC, I wanted to utilize the latest tech in React for the majority of the view layer. Aside from using controllers as just routing, this structure would make it difficult to pass data from Rails to React. With GraphQL, however, I could skip having to create API endpoints that essentially would directly expose the models.

This gem graphql made it very easy to put a thin layer in front the models that apollo could integrate with. Having query endpoints were as simple as defining a strong typed schema, and adding a method to utilize them. Mutations could be a little more tedious but follow similar developer experience to writing post/put endpoints. Testing these methods, also, were much more straightforward as I could test the method itself rather than making mocked requests and requiring other tedious test setup.

And, lastly, not related to graphql-ruby specifically, but working with Apollo was a treat. Modern useQuery React hooks made declarative data fetching nearly seamless!

import * as React from 'react';
import { useQuery } from 'react-apollo';
import gql from 'graphql-tag';

import { PostType } from '../interfaces/PostType';
import { InstagramPostType } from '../interfaces/InstagramPostType';
import InstagramPost from './InstagramPost';

const InstagramPostsQuery = gql`
{
  instagramPosts {
    id,
    mediaType,
    mediaUrl,
    thumbnailUrl,
    timestamp
  }
}
`;

interface FeedProps {
  posts: PostType[];
}

export default ({ posts = [] }: FeedProps) => {
  const { data, loading } = useQuery(InstagramPostsQuery);
  const instagramPosts: InstagramPostType[] = data?.instagramPosts;

  return (
    <div className="container">
      <div className="row justify-content-around">
        {
          !loading && instagramPosts
            .filter((igp) => posts.map((p) => p.instagramId).includes(igp.id))
            .map((igp) => (
              <div key={igp.id} className="Post col-6 col-lg-4 mb-3">
                <InstagramPost instagramPost={igp} />
              </div>
            ))
        }
      </div>
    </div>
  );
};

Code Splitting for free

In Rails 6, the latest major version, Webpacker has been standardized and provides a much better build pipeline experience than Sprockets. The concept of “packs” allow the developer to have isolated app-like bundles per view, while also allowing flexible component sharing/imports across pages.

For example, the main admin dashboard view for Record Temps ended up just being:

<div id="root"></div>
<%= javascript_pack_tag 'posts'%>

This view provided an empty div for the react component to mount to, and then uses a Rails function javascript_pack_tag to identify which pack we want to use.

When Webpacker compiles, it creates a hashed bundle for every pack that we have as minimized and split as possible! While this is doable in React with React.lazy, etc, this experience is nice to be standardized as a best practice and setup with the initial Rails app scaffolding.

RSpec

Jest and react-testing-library have come a long way and now provide an excellent testing experience, but I just wanted to note the de facto test runner in Rails world, RSpec, is extremely mature and well documented. After getting used to it, professionally, I have to come to like how Rails apps can be covered in this opinionated structure.


Cons

Dev Ops

This was my first time trying to have a full CI/CD setup with Ruby/Rails which presented problem after problem. Rails is notorious for being difficult to setup on local machines, even when using latest version of anything. I tried using AWS EB and more generic Docker configs, but having the right dependencies for a memory-heavy app made this process difficult and slow. Even being on the latest version Rails (6) and latest bundler seemed vastly unsupported on the major cloud providers.

I ended up going with Heroku which ended up being turn-key and a pleasure to use. I do not like the “black box” sense that comes with Heroku but it seems to work, and the attitude of Rails as a whole is just let the magic work itself.

Also, when I have to set up a new Rails app, I would recommend doing the dev ops immediately with CD. Incorporating just a few well known gems immediately made everything more complex, and I may have started the dev ops after about 10% of starting the project!

Difficult to do data transfer between Rails <> React

While working with Rails or React is really well done, there is some friction involved with using a mixture of both per view. Just dropping in a few React components with some co-mingled views left some problems with sharing data from Rails as props to React. This left you with a few options such as using GraphQL to query the data you need, but that would mean twice the setup as you have to provide the data for Rails views from instance variables as well.

The nicest solution to this I could find ended up being hydrating the React apps with inlined, serialized props from the Rails instance variables. This also sandboxed the React apps well (with Typescript) as you could be sure you have the right props and test them more easily knowing you will have the data you expect passed down.

Furthermore, if the page would be interactive-heavy, I would elect to go SPA for that and use GraphQL the whole way.

Hard to share SCSS/easy to duplicate

Similar to sharing data with React components seamlessly, styling left some problems as well. Using SCSS with Webpacker meant that compiled stylesheets would be included on every page. Therefore, it makes the most sense to use the (now) archaic practice of having global stylesheets that include all the cons of cascading. This is not that bad though and more of a typical flow.

Getting to use more recent technology such as CSS modules though solve a lot of problems that global stylesheets have, but if you mixed and matched in a stack like this, you would be duplicating a lot of the CSS burden of the end user. And, if you keep the SCSS and CSS modules separated, too, then you lose access to your SCSS theme and/or variables (I used bootstrap so having those variables is normal practice). Maintaining these variables in multiple places is just that much more of a burden on the developer!

Conclusion

All in all, this was an extremely fun stack to work with that I would definitely recommend to anyone, especially to experienced Ruby developers. It provides a nice mixture of ordained, mature practices, while providing an interface to more delightful, modern frontend tech like React/GraphQL!

Thanks for reading! Feel free to reach out on Twitter if you have similar experience!


Written by Joseph Furlott who lives and works in Brooklyn, New York. I am a software engineer that specializes in designing and building web applications using React. I work at Datadog as a software engineer.

© 2022
jmfurlott@gmail.com