Transitioning to Relay Modern at ONEHOPE Wine
ONEHOPE released our e-commerce platform built on React, Relay, GraphQL, FlowType, and NodeJS in January 2017.
We chose GraphQL because we manage over 14 NodeJS microservices in a distributed systems cluster and we needed a sane way to access these microservices. GraphQL provides the following for us:
- Acts as an API gateway to these services
- Organizes server payloads and applies data transformations over them for our frontend client to consume
- Provides documentation on API data through GraphiQL
- Graceful deprecation of APIs and fields of APIs, which allows us to iterate on our backend quickly in a safe manner.
Anecdotally, the Relay/GraphQL developer experience is better than Redux. Relay gives us a lot of free wins and features out of the box such as:
- Optimistic updates. Relay allows us to make local changes to the store on the assumption that our API call will succeed. This allows us to update the UI immediately while waiting for the server to respond back.
- Fetching only what the component needs in an intelligent way. Frontend developers never wrestled with how to transform server data to fit React component data requirements. GraphQL and Relay handled that for us.
- Colocation of data requirements and React views which give developers clearer understanding and consistent predictability of how the React component interacts with the overall system. We know exactly which React component will change because of a backend or GraphQL change. With clients hooked up to a REST API, you wouldn’t know which pages broke because of a change in API payload unless you have thorough test coverage.
- Type safety of client-side data via Relay fragments that are based on a typed GraphQL schema.
- A lot less boilerplate code needed to be written to pass backend data into React components compared to Redux
When I took a look at the new Relay Modern API, I got excited.
There’s a notion of “mutations” in the GraphQL ecosystem, which is how one does writes in a system that uses GraphQL. One big win is that mutations in Relay Modern were simpler to write than Relay Classic. No more “fat queries” and overloaded mutator configurations to get a mutation to work.
Fat queries were supposed to specify the limits of how much data a mutation was supposed to bring back. Relay Classic has a collection of GraphQL queries that were fetched and stored in its cache called tracked queries. By intersecting the fat query and tracked queries, Classic can query exactly what the user needs from the mutation and nothing more.
Since Relay Classic optimize the queries for us, we defaulted to having the fat query grab everything and allow Classic’s tracked queries determine what data was necessary. We found that there was little advantage to think about limiting our fat query. It seemed to be an unnecessary part of writing a mutation.
Classic mutator configurations such as
FIELDS_CHANGE made making changes to the store seem magical and black box. Modern gives us granular control over the Relay store when mutations occur via its imperative-style API.
The Modern mutation implementation is an overall win in developer experience. It is simpler to understand functions as opposed to ES6 classes. The Modern mutation seems more verbose because the mutation query (the data set requested by the client to the backend to fulfill a UI change) has to be explicitly set. However, the performance gains from having static queries are worth looking over that minor set back.
Static queries mean that all GraphQL queries for an app are determined ahead of time. GraphQL queries in Relay Modern are created at compile time, and not runtime like in Classic. This allows for dramatic performance enhancements when fetching queries and committing mutations since all the work of computing queries is done already and the client does not have to do it. This also opens up for other optimizations such as persisted queries; which reduces the payload that the client has to send over, and reduces attack surfaces on our APIs since we can whitelist queries based on the hash being sent over the wire.
- Got started according to the docs
- Switched our routing solution from react-router-relay to found
- Ran the migrate-to-modern codemod one feature at a time
- Fix the feature folder just enough to get it to render. Comment out all mutations.
- Repeat 2 and 3 until the entire code base is using Modern and we can deprecate Classic.
- Go back to each feature folder and get it to work just as well as it was in production, which includes manually rewrite all Relay mutations.
We didn’t use compat mode because it took very little time to convert Classic components to Modern. We were able to do the conversion while also developing new features in parallel.
The process was painless thanks to the Relay codemods. There were a slight learning curve and time investment in rewriting our mutations to use the imperative API as opposed to the mutator configurations, but there is a good amount of examples in the source code to help you get up and running in learning the new API.
The conversion was an overall net positive.
Greater Certainty of Relay Cache State
We’ve spent hours in the past scratching our heads why our Classic store wasn’t updating the way we wanted. We littered our mutations with
REQUIRED_CHILDREN in hopes that it would get us the changes in the store we needed. The combination of being able to explicitly state what GraphQL fragments we needed and how it should affect the Relay cache allows us to create more responsive and immersive UI experiences that were difficult to create in Classic; with more confidence that the store will be updated in the way we want.
Our Modern production environment had:
- 1.25x-2x faster initial network calls
- mutations resolving 3x faster
- 200ms faster initial paint
than the Classic Production app on desktop. It’s interesting to note that the Modern development environment is consistently faster than the Classic production environment across every metric. We would imagine the performance gains on mobile to be much more dramatic.
We are pleased with working with Relay Modern and we are excited about the potential tooling and features that can be created with the
relay-runtime packages. With
graphql-compiler, we could create a way to generate TypeScript typings for Relay components much like how it is generating Flow typings today. With
relay-runtime, there are primitives for offline caching, which opens opportunities to better serve devices with spotty internet.
We are looking forward to seeing and contributing to how the GraphQL ecosystem develop and the innovation that will come out of GraphQL clients.
Check out our e-commerce platform written in Relay Modern here!