GraphQL & React tutorial (part 5/6)
Combining components into screens and mocking our server
GraphQL & React tutorial is a series of posts following the development process of an βInbox Zeroβ style todo list application, built in JavaScript with the most innovative and production ready tools as of early 2017. Parts: 1, 2, 3, 4, 5, 6.
So far in this series weβve concentrated on the smallest possible unit of UI, and built from the component up. Doing so has allowed us to develop each component in isolation, figure out its data needs, and play with it in a component explorer without needing to stand up a server or build out screens.
In this post we will start combining components together and fetching data for them. Weβll build the screens of our app, but do so without requiring a server yet.
A GraphQL-powered Taskbox
Weβll focus on the inbox screen in this post, although the other screens will be similar in approach. To get started on the inbox, weβll follow a βpresentational/containerβ component split and build a βpureβ presentational inbox and then use Apolloβs (our choice of GraphQL library) graphql
higher-order component [HoC] to build the container.
The presentational component is simply a combination of other components, and we can develop it in exactly the same way as we developed our components in the first place; using React Storybook to drive different use cases.
We can start by building a set of stories for the different states a user could be in looking at the homepage:
This isnβt particularly different to how we build the task componentβs stories in part 4, and again, we are more or less following the same process. We have also built stories to cope with more βimperfectβ statesβββwhat happens while the data is loading, or what about if there is an error? Building a complex component inside a component explorer in this way is great because such states are often overlooked or difficult to build against in a traditional process.
Once weβve built out those stories, we can use them to build out the component:
Again, the component isnβt too complicated, and we are careful to deal with all of the different states weβve outlined in our stories. Running the states in storybook makes it easy to test weβve done this correctly:
You may notice that this story generates tasks in a very similar way to the Task.story.js
file in the previous part. It makes sense to refactor that logic out into a single test helper utility. In fact, as I alluded to then, it would be cool to have a mocking utility which automatically creates such a function from a GraphQL fragment!
Wire in data
The Inbox
component isnβt really all that different to any other component that weβve built so far in this process (although not very reusable given it renders quite a specific output for the inbox screen). To actually use the component we will need to provide it with data, and thatβs exactly what our InboxScreen
container will do.
A container is a component that renders a single presentational component, whilst doing the βimpureβ work for that component, such as asynchronously fetching data.
In our case, our container will be generated by Apolloβs graphql
higher-order component, which does the work of sending queries to our GraphQL server and handing off the results to our component when they are ready.
First, we install the Apollo libraries we will need:
yarn add apollo-client react-apollo graphql-tag
Then we use them:
The above code attaches a query to fetch our pinned and inbox tasks, sets some polling options, and passes that data through as props
to our βpureβ Inbox
component. Letβs look at each step in a little more detail.
The query is short but has a few interesting things going on:
- We name the query
InboxQuery
. Strictly we donβt need to do this, but it helps debugging across our stack to know which query we are looking at and where it comes from. A convention is useful here. - We use the
me { tasks(state: ...) }
field that we set up to get the list of tasks of a given type assigned to the current user. As we are using it twice in this query (once for the pinned tasks and once for the inbox tasks), we use an alias (pinnedTasks: tasks(...
) to βrenameβ the field. - We re-use the
TaskList.fragments.task
fragment (which built off theTask.fragments.task
fragment) so that this component doesnβt need to concern itself with which fields itβs subcomponents will need; this fragment mechanism allows some data-fetching concerns to stay at the component level.
In the options
argument we set the query to always fetch (so it doesnβt hit Apolloβs cache if we come back to this screen), and to poll every ten seconds.
Finally the props
function allows us to control the data that is passed to the Inbox
component. The argument to the props
function is the default prop set by the graphql
container (to put everything on a single data
prop) and the return value of the props
function is what will actually get passed. So we can use this function to retain control over the shape of the props of our Inbox
component, instead of having that dictated by the particular library we are using.
Add a mutation
Apart from the two lists of tasks, we also need to provide our other arguments to the page, which allow the user to move tasks between lists. To do so, theyβll need to call a GraphQL mutation on the server. We can attach a mutation as a callback to a component using the the graphql
HoC also:
We attach the mutation similarly, by creating a withOnSnoozeTask
HoC using the graphql
function. The mutation takes a single variable taskId
. Our props
function takes the βrawβ mutate
callback provided by graphql
and renames it onSnoozeTask
as well as making it take a single String argument rather than an object of options (which is what the mutate
function expects). This means that the Inbox
will get passed a prop called onSnoozeTask
, as it expects.
Notice as well that we re-use the TaskList.fragments.task
fragment in the mutation query so that when the mutation returns from the server we get all the same fields of the changed task. This is important because it means weβll always have all the correct fields in Apolloβs document store for tasks; in the future weβll use this to dynamically and optimistically change the task list with the mutation results; for now we just use the refetchQueries
option to ensure that we re-query all the data again whenever we make a change (weβll work on making this better in our next post once we have a server to dynamically change data on).
Test the GraphQL container
As our container component expects to communicate with an external GraphQL server, it is more difficult to test than our simple βprops to HTMLβ presentational components. However we can still test it within our component explorer by using a mocked Apollo client.
We need to use a few tools to do this:
yarn add --dev graphql graphql-tools apollo-test-utils
Then we can follow Jonas Helferβs technique to build a testing network interface which returns mocked data based on our schema.
We start by adding our schema (which we developed in part 3) to the frontend app in the file src/schema.js
. We need to wrap the raw βSchema Languageβ of this file to turn it into JS:
Now we will build a custom network interface for Apollo that uses the types in that schema to automatically build test data. We do want to control one piece of dataβββwe want the output of the tasks
resolver on me
to return a fixed set of tasks for each of our stories.
So we write:
We use the mockedTasks
variable to act as our mocked βserverβ. Each test initializes the state of the server and then the client will query the server by calling the resolvers that we have provided.
In many ways these stories closely parallel the stories we wrote for Inbox
above, which were simpler because we could specify the inboxTasks
and pinnedTasks
props directly. Given that InboxScreen
is simply a code-only (with no extra UI) wrapper for the Inbox
, we could convert these stories into purely automated tests and not access them in Storybook. In that case, we would simply check that for the given mocked response, our contained Inbox
component gets the props
we expect at the times we expect. Weβll consider such a test case in the next part of this series.
Testing Mutations
One interesting thing we can do with the above approach is provide a resolver for the updateTask
mutation. When we test the story above, we can click the snooze button and see the "updateTask"
action logged in React Storybook.
We could take this further and actually update our mockedTasks
βserverβ when we run the mutation. This seems a little like overkill at this stage, but could be a good candidate for automated tests, to ensure that more complicated mutation behaviours (such as query reducers/updaters and optimistic UI) work correctly.
Build the app using React Router v4
Now weβve built a screen of our app and wired it up with data, we are at the point where we can put the entire frontend app together.
Our app is web-based so weβll associate each screen of the application with a URL pathβββthe screen weβve been building (the inbox) at the root path (/
), and the other lists at corresponding URLs, i.e./snoozed
and /archived
. To achieve this, weβll use the defacto URL router for React, React Router, and in particular itβs v4 API (which is in beta as of this writing).
To install the router, we can run
yarn add react-router-dom@next
Using the router is actually quite simple, we just layout our main page layout, and use the Route
component to choose which component to display in each position within that layout. In our case, we have a simple fixed menu on the left and then our different task listing screens on the right. So our App
component looks like:
Each of the βScreenβ components is responsible for fetching its own data, and we will render the MenuScreen
as well as one of the InboxScreen
, SnoozedScreen
, ArchivedScreen
depending on which URL we are at.
Our App
component above expects to be rendered in a specific context (a context
in React is like a set of βhiddenβ props
that are available all the way down the tree; it allows things like the Match
component above to read route data from the Router
component without having to wire everything together). In our case, it expects route information and an Apollo client to be available.
To achieve the above, we update the src/index.js
file that was created by create-react-app
for us to set those things up:
Note that the above wonβt really work properly yet as we donβt yet have a server yet, but itβs instructive to understand where we are heading.
Test the App component
We can the App
component in exactly the same way that we tested our container components earlier. We can write stories that both mock out the GraphQL server responses and the router state.
For instance, if we wanted to write a story for the homepage, it would look like:
This test is very similar to the test we built for the InboxScreen
but lets us render the complete app with a fixed set of data and test that it looks OK. Weβve managed to get to the point of rendering the complete app, with sensible data, without ever touching the server. Hopefully if weβve designed our schema well, weβll be able to slot the real server in and things will just work!
Next Steps
Weβve now built our frontend application all the way out to the level of a complete routed application that passes data all the way down the stack without even touching or configuring a server. This has worked great for ensuring that static data renders the correct way and that routes work correctly.
However, the time has come to build a GraphQL server. In the final post of this series, weβll use a simple server generating tool called create-graphql-server
to generate this server, and start using real data with our application.