I’ve spent a great deal of my writing time in the past few years arguing for GraphQL in my GraphQL for Rails Developers and its frontend companion Apollo in the Apollo Handbook. I think these are very good tools for providing a clear separation between an API layer and a frontend layer.

But in saying that, I acknowledge there is no silver bullet for software development. So what would I do if I couldn’t use React or GraphQL?

To replace React on the frontend, I would use View Component as I have written about here and here. I could also be convinced to use Phlex.

I think having a typed layer between your database and view is just something that makes sense, and so to that end I would define a separate class for the data types for these components, using dry-types and then pass objects of those classes to the view, in a way that if you squint hard enough you could see it as the Presenter Pattern. I proposed something similar to this two years ago in my “Typed View Components” post

Riffing on the example from that post, I would have this as:

class RefundComponent < ViewComponent::Base
  extend Dry::Initializer
  Types = Dry.Types()

  class Refund < Dry::Struct
    schema schema.strict

    attribute :standalone, Types::Bool
    attribute :amount, Types::Float
    attribute :currency, Types::String
  end

  option :refund, Refund
end

This allows you to keep together the logic of the component (both its Ruby code and its associated views) and the presenter in one directory.

In the controller, the code would look like this:

refund = RefundComponent::Refund.new(
  standalone: @refund.standalone?
  amount: @refund.amount,
  currency: @refund.currency,
)

@refund_component = RefundComponent.new(refund: refund)

This would still give us an interface similar to GraphQL, where the connecting layer between the database and the frontend is still typed. I think it’s teetering on the edge of being too verbose, but in all things trade-offs.

You then don’t end up exposing any way of doing database queries to the view, which would help prevent N+1 queries. And you can test your views in isolation from the database too. The refund passed to the component doesn’t have to come from the database; it could be a stubbed object, as long as it responds to the right methods.

In the view file itself you might or might not get smart tab-completion like you do within TypeScript-powered GraphQL code, but I think that’s a fair trade-off.

This whole approach trades off React’s “reactivity” as well, so there’s no state management going on here or DOM updating when the state changes. There are probably ways around this (like Hotwire, etc.) but I haven’t gone down those paths yet.

Another benefit here is that all the code is in one language, rather than three (Ruby, GraphQL and TypeScript), and that might make it easier for frontend-adverse people to pick it up as well.