A pragmatic guide to maintainable React components powered by GraphQL.
Writing performant, testable, and readable React can be thought of as an art, but instead, I hope to convince you that it can instead be achieved with a simple toolkit ๐ง , a few recipes ๐งพ , and some examples ๐งช .
Starting with GraphQL
One common approach and pitfall is to start implementation from the front-end and work backward towards your GraphQL API. In my experience, this can lead towards less-than-ideal schema design, negatively impacting the GraphQL experience for any other consumer of your API, where types end up as sacks of data void of any meaning and without purpose.
Things to consider include:
- How will other frontend views get this data?
- How will nullability be applied?
- Will the data make sense to other developers?
Starting with GraphQL will not only be better for your schema, but it will also be better and crucial for you and your team when building maintainable front-end systems. To start building your GraphQL API and building a truly resilient schema, you should almost certainly consult Production Ready GraphQL, and a worthy companion tutorial; Designing a GraphQL API.
Making the jump
Now that we've got a production-ready GraphQL schema, how do we implement its consuming counterpart? Let's start with the most fundamental components in our larger application and explore some of the first tools in your toolbox ๐ง ; component composition and GraphQL fragments.
Fragments let you construct sets of fields, and then include them in queries where you need to.
Fragments can be used to achieve one concept commonly used in React; collocation, where you're able to contain all of the data needed from a component alongside your logic, styling, and rendering.
In practice, you're able to create a component that doesn't need to query its data but instead provides an interface to how it should receive its data when used in other components (hence, component composition).
Example ๐งช
A small component that receives all of its data from props and defines how it expects to receive data using a GraphQL fragment.
Is this thing on?
To build maintainable and shippable React, you'll want to test functionality in isolation in a way that makes it very clear when things break so that you and your team will be confident with every release.
Testing React components isn't as hard as it's made to be, especially with modern-day tools such as React Testing Library, and Jest, which take out all the guesswork of rendering and interacting with a component. Jest will act as the test runner and provide the overall structure of your unit tests, while React Testing Library will provide the methods needed to accurately assert functionality.
User functionality
Getting started with React Testing Library is quick and no different than writing unit tests for functions and classes. Every test case should start by rendering a component with the render method and destructuring the return to get access to the rendered result, and document queries such as getByText, getByLabel (see the query cheat sheet).
Example ๐งช
An example test file using React Testing Library and our previously defined Message
component. Showing how one could test user functionality with predictable results.
Cloudy with a chance of bugs โ ๐๐
Jest uses a package called Istanbul to provide test coverage metrics such as statement, branch, function, and line coverage so that you can understand and enforce the quality of your test suite, providing more confidence in releases.
It's highly recommended you set coverage thresholds that make you comfortable and don't burden you with covering every test case and flow, and subsequently integrate coverage tests into your CI/CD pipeline and block failing builds.
To get started and collect test coverage, you can run the following command (or add the --coverage argument to your existing node test script):
npx jest --coverage
Some things to keep in mind when trying to achieve high coverage:
- test units should interact with your component using every variation
- if and early return statements need to be considered and both or multiple paths need to be tested
- optional chaining (?.) gets increasingly harder to test the longer the chain and GraphQL type nullability should be a factor to consider when building your component
At Jobber, our test coverage targets sit around 85%!
Putting the pieces together ๐งฉ
Now that we've defined our component and how it will get its data, how can we use it at scale and alongside other components? Let's take a look at the loader concept and how to bubble up your fragment!
Example ๐งช
Multiple components showing how one can reuse and build larger components made up of smaller components that implement fragment collocation and data fetching (using a loader component). Using the previously defined Message
component.
Using the loader
pattern, we can easily implement and test our Conversation
component and handle query transformation in our loader component. In addition, we don't need to mock as much using MockedProvider
from Apollo (see testing react components using Apollo). In addition, queries or fragments will be much easier to add when using a tool such as GraphQL Code Generator our Typescript types become very easy to implement!
About Jobber
Interested in React & GraphQL? Consider joining Jobber or contributing to our open-source design system Atlantis. We're hiring for remote positions across Canada at all software engineering levels!
Our awesome Jobber technology teams span across Payments, Infrastructure, AI/ML, Business Workflows & Communications. We work on cutting edge & modern tech stacks using React, React Native, Ruby on Rails, & GraphQL.
If you want to be a part of a collaborative work culture, help small home service businesses scale and create a positive impact on our communities, then visit our careers site to learn more!help small home service businesses scale and create a positive impact on our communities, then visit our careers site to learn more!
Top comments (0)