Building Signals provided the team at Bonusly a chance to work with a new tech stack. Our team chose to build on top of our core competency in Ruby on Rails with a GraphQL service using GraphQL Ruby. GraphQL provided new and interesting ways to access and manipulate data. It also coordinated with our adoption of a single page React user interface.

As we built our GraphQL application we learned some lessons in patterns to avoid.

Don’t Treat Queries and Mutations like REST

React hooks make it easy to build reusable utilities in a React application. Apollo GraphQL provides hooks for querying and mutating data. We began to build our own hooks for accessing our data. A pattern emerged from this practice where we would build a single hook for querying a type of data from our graph.

const query = gql`
query User($userId: ID!) {
  user(userId: $userId) {
    id
    email
    name
    admin
  }
}
`;

const useUser = (userId: string) => {
    return useQuery(query, { variables: { userId }});
};

We began to use these same, core queries throughout our application. As we used the queries in different places, we added fields we needed to those queries. The queries grew to be massive and used throughout the application.

Unfortunately this meant we weren’t making use of one of the key features of GraphQL: the ability to tailor data to specific use cases within the application. User facing queries were loading data that only admin facing components needed and vice versa.

When building an application, queries should be specific to the data needs of a component. We have changed our practices to encourage writing component specific queries which only load the data required.

Don’t Assume Your Data Shapes are the Same Everywhere

Our initial queries made it easy to build out TypeScript types for our query results. Because we used the same queries throughout the application, we built out a set of core model interfaces.

Even before adopting a more component specific querying pattern, we began to see issues with this approach. Because of the graph nature of GraphQL data, nested data did not always share the same fields with other instances of data of the same types. Our types misrepresented what fields were available to work with. We encountered bugs where we expected data to be present when it was not at runtime.

To remedy this issue we adopted types generated from our GraphQL documents. GraphQL code generator inspects GraphQL documents and uses type information from your schema to generate code. The generated code can include types, data-loading hooks, and more.

We’ve found that this generated code speeds up our ability to write new queries with well typed data.

Consider How to Handle Context Specific Relationships

Our data is relational and lends itself well to a graph. Representing relational data in GraphQL allows queries to traverse deep relationships with ease.

One problem that emerged was when a view of data depends on relationships between more than two pieces of data.

Data graph of relationships between surveys, questions, and responses

In our survey application we have survey objects which have a set of questions. These questions are also related to other survey objects. When a user fills out a question in the survey, they create a response object associated with the specific survey and the question. We cannot express the relationship between a question and the user’s response as a field without being in the context of the survey.

We chose to store a reference to the survey in which the question is being resolved in the GraphQL context. This provided a workable solution for the initial way that we queried for this data.

We found that we needed to query for this relationship without the specific query structure we expected. This meant that because we did not have the proper context, our field resolved as null.

Our interim solution was to provide an argument on the field which would allow us to narrow responses by a survey ID.

A better long-term solution would be to rework our graph from the top down to express the relationship between surveys and questions as an edge. An edge represents the connection between two nodes or points in a graph. An edge also expresses contextual data based on the connection between the two nodes.

Response as a field on the edge between survey and question

We could express the current user’s response on the edge between a survey and a question.

Don’t Build Too Many Stateful Components

Building React applications using Presentational and Container components became a popular pattern. It helped organize the complexity of an application in an easy to reason about way. This pattern divides components into two categories: how things look and how things work.

This approach is now viewed as dogmatic and somewhat irrelevant with the adoption of React hooks. Hooks abstract business logic into reusable functions and keep components functional and easy to read.

Despite the change in view on this pattern, there is still value in clear responsibility. We embedded business logic throughout our application components.

As our application grew and we found it difficult to adopt and reuse components because of their embedded business logic. We created interfaces for passing down mutations through components but they were brittle and use case specific.

Our solution to this problem has been to re-adopt Presentational and Container components. React hooks and GraphQL’s ability to express data shapes through fragments makes this easier to adopt.

Hooks are Here to Stay

While we may have hit some speed bumps in our adoption of bleeding-edge React practices, we’ve ultimately found them to be much easier to work with. React hooks are much easier to reason about and are easily composable.