Unsuck SPAs With GraphQL And TypeScript

Martin Lipták
6 min readDec 24, 2019

I'd been a happy Ruby on Rails developer for years when I started playing around with SPAs. Server-rendered HTML with sprinkles of jQuery worked well for most of my projects, but some parts were getting too complex. That's when my React journey began.

What makes the perfect cocktail? (Rovinj, Croatia)

Note: Check out my newer article on the topic.

React with Redux was the most popular SPA combo at the time and I gave it a try. Well, the amount of boilerplate that Redux required really sucked. Having been used to Rails, everything seemed 10-times more complicated than it had to be and I couldn't see too much of added value to make up for it. I had a feeling that I would rather deal with occasional jQuery spaghetti than write all the Redux bullshit. For example, listing products would mean:

  1. Make an API endpoint.
  2. Add list products query fetch/loading/success/error actions.
  3. Add products resolver that handles all these actions.
  4. Bind actions with the React component.
  5. Bind products state with the React component.
  6. Trigger fetch action in the React component componentDidMount.
  7. Render loading/success/error states.

On another project, I tried luck with MobX. Things were becoming simple again, but only up to a certain point. I still had to think about API and data management on the frontend. Let's get back to our products example:

  1. Make an API endpoint.
  2. Add products store with data, loading and error properties and a method that fetches data and updates these properties.
  3. Bind products store with React component.
  4. Trigger the fetch method on React component load.
  5. Render query loading/success/error state.

MobX was much less verbose than Redux, but I still had to think about too much unnecessary stuff. Could we somehow automate this repetition?

GraphQL to the Rescue

GraphQL replaces REST API making developer's life so much easier. We just define data and it takes care of everything else.

class Type::ProductType < Types::BaseObject
description "Type-level documentation..."
field :id, GraphQL::Types::ID, null: false
field :name, String, "Field-level documentation...", null: false
field :price, Number, null: false
field :variants, [Types::VariantType], null: false
field :image_url, String, null: true
#
# Field resolution falls back to the underlying `Product` model.
# For `variants` this causes an N+1 problem because variants are
# loaded for each product. This is where custom resolution
# comes in handy.
#
# def variants
# authorize_and_lazy_load_association :variants
# end
#
# Now `variants` method is called instead of the original
# ActiveRecord association. It ensures record-level authorization
# and loads the association lazily to avoid N+1.
#
# We can build DSLs that define fields and resolver methods.
#
# association :variants
#
end
class Types::QueryType < Types::BaseObject
field :products, [Types::ProductType], null: false do
argument :first, String, required: false
argument :take, Number, required: false
end
# Root type has no underlying model so we need to define
# resolvers for all its fields.
def products(first: nil, take: 10)
Product.authorized_for(context.user).paginate(first, take)
end
# On the same note, we can build DSLs to avoid repetition.
# paginated_resource :products
end

And now we can run our first query.

query Products {
products(take: 20) {
id
name
price
variants {
id
name
}
imageUrl
}
}

And get our data.

{
"products": [
{
"id": 1,
"name": "Redux Sucks T-Shirt",
"price": 19.99,
"variants": [
{
"id": 1,
"name": "S"
},
...
],
"imageUrl": null
},
...
]
}

Mutations are analogical to queries, but they're used to update data or perform actions on the backend. They take in arguments and usually return changed values that are subsequently updated in frontend components (check out graphql-ruby gem for more details about mutations, authorization and lazy loading).

GraphQL gives your API conventions, generates documentation (see Github GraphQL API) and validates types. Speaking of types, why is everyone so excited about them?

Hello TypeScript

JavaScript has been a pain to work with not only for its lack of advanced features, but also a very loose type system. For example, Ruby is a very dynamic language, but it still has some runtime type safety. If you access a model attribute that doesn't exist, it throws an error and tests fail (in case you have them). If you read an undefined property in Javascript, you get undefined and that's what the user sees on the page unless you test that specific value on a rendered page.

TypeScript is a superset of JavaScript with all its stable improvements adopted sooner than ES* standards. It has private fields, enums, interfaces, optional chaining, decorators, etc. It's type system is very smart so you mostly only need to type function arguments, everything else gets inferred (unlike Java 😃). null and undefined are considered separate types (in strict mode) so when something is a String, its value is always defined (unlike Java where anything can be null and you can guess what's the most common runtime error 😃).

const person = {
name: "John Doe",
email: "john.doe@example.com"
}
console.log(person.emailAddress.toLowerCase())// TypeScript: `emailAddress` doesn't exist. Did you mean `email`?
// JavaScript: Runtime error.
const printEmail = (email: String | null) => {
console.log(email.toLowerCase())
}
// TypeScript: `email` can be `null`. You didn't handle this case,
// did you?
// JavaScript: Runtime error when email is `null`.
const printEmail = (email: String | null) => {
console.log(email ? email.toLowerCase() : "NO EMAIL")
}
// TypeScript: All good.
// JavaScript: Finally, no error in production.

Let's get back to GraphQL.

query Products {
products(take: 20) {
id
name
price
variants {
id
name
}
imageUrl
}
}

Graphql Code Generator validates our query against the backend and generates Apollo hooks to load data.

import React from "react"
import className from "classname"
import { useProductsQuery } from "./generated-graphql-hooks.ts"
...
interface Props {
size: "small" | "large"
color: "red" | "green" | "blue"
}
const ProductsBlock = ({ size, color }: Props) => {
const { data, loading, error } = useProductsQuery()
if (loading) return <Loading />
if (error) return <Error error={error} />
return (
<div className={className("block", { size, color })}>
<h1>Products</h1>

{data.products.map(({ id, name, imageUrl, price }) =>
<div key={id}>
<h2>{name}</h2>
<Image src={imageUrl ?? "no-product-image.png"} />
<Currency>{price}</Currency>
</div>
)}
</div>
)
}
export default ProductsBlock

We only had to write one type definition in the whole file that actually documents component properties. All imported libraries have their type definitions (all common packages have them nowadays) and useProductsQuery comes from our generated types.

If you aren't familiar with React hooks, useProductsQuery returns state values. When they change, component re-renders with new values. data contains types we defined in Ruby. If we mistype a property (for example, prodcuts instead of products), TypeScript will complain. If we don't provide a default value to imageUrl, since it's optional in Ruby types, you can guess it, TypeScript will complain. Editors supporting TypeScript will show errors during typing and autocomplete everything they can.

Now imagine renaming an attribute on the backend or making a required attribute optional. Type generator will complain about every query referencing the renamed attribute. You fix these queries. Then TypeScript will complain about all the places where the renamed attribute is used or the optional attribute being null isn't handled. TypeError: X doesn't exist on null was the single most common error I would see every now and then. In TypeScript codebases it rarely happens.

Conclusion

So how do we list products with GraphQL?

  1. Add product type and query -> products resolver.
  2. Add products query and generate types.
  3. Add component that uses the hook to render loading/success/error states.

Way easier than with Redux or MobX, but how does GraphQL compare to rendering HTML in Rails?

  1. Add products controller and index action.
  2. Add products/index view listing all the products.
  3. Sprinkle some JavaScript (for things like product image gallery or add to cart button).

Rails goes a long way to make server-side rendered pages respond fast (Turblinks) and help you write well-organized frontend code (Stimulus). A lot of people are happy building even more challenging interactions with Rails.

Both approaches can achieve similar results. The Rails way is easier in the beginning, but can get trickier later. Decoupled frontend is no longer a mixture of HTML and JavaScript, but a single TypeScript codebase. Frontend developers no longer copy-paste HTML, but use component libraries. They don't think in terms of divs that need to be updated, but states that must be rendered. When we already have a backend API, it can be re-used by mobile apps or anything else. GraphQL does bring some overhead, but it can be worth it for bigger projects.

What I like about GraphQL is that frontend doesn't have to duplicate any backend logic. React components just replace Rails views, partials and helpers. Each page component requests data it needs and shows them to the user while all the logic lives on the backend. And TypeScript makes sure that frontend works and is compatible with the backend.

--

--

Martin Lipták

I'm a software developer who loves creating applications that improve people's lives. I also enjoy travelling, learning languages and meeting people.