This post is a step-by-step tutorial with the goal of building a simple Instagram application from scratch using create-react-app.
⚠️ This tutorial is outdated! Check out the Prisma examples to learn how to build GraphQL servers with a database. ⚠️
Realy Modern is the very promising evolution and the 1.0-release of Facebook’s homegrown GraphQL client Relay. It was announced at this year’s F8 conference and officially released by Lee Byron during his talk at React Europe.
If you’re just getting started with GraphQL, check out the How to GraphQL fullstack tutorial website for a holistic and in-depth learning experience.
This post is a step-by-step tutorial with the goal of building a simple Instagram application from scratch using create-react-app
. You can take a look at the final version of the code here, just follow the instructions in the README
to get up-and-running.
Relay — A Brief History
Relay and Apollo are currently the most popular and sophisticated GraphQL clients available.
Apollo is a very community-driven effort to build a flexible and easy-to-understand client that let’s get you get started quickly with GraphQL on the frontend. In only a bit more than a year it has become a powerful solution for people looking to use GraphQL in web (and mobile!) projects.
Relay on the other hand is a project whose key ideas grew while Facebook was using the early versions of GraphQL in their native mobile apps starting 2012. Facebook took the learnings they gathered from using GraphQL in their native apps to build a declarative data management framework that integrates well with React. For Relay becoming open-source, much like with GraphQL, it was pulled out of Facebook’s infrastructure with the ambition to build a data loading and storage solution that would also work in non-Facebook projects.
Apollo and Relay have different focus areas. Where Apollo optimizes for flexibilty and simplicity, one of Relay’s key goals is performance.
Relay Modern […] incorporates the learnings and best practices from classic Relay, our native mobile GraphQL clients, and the GraphQL community. Relay Modern retains the best parts of Relay — colocated data and view definitions, declarative data fetching — while also simplifying the API, adding features, improving performance, and reducing the size of the framework. Relay Modern: Simpler, faster, more extensible
If you want to take a deep-dive into Relay Modern, make sure to check out this article on the Apollo blog.
1. Preparing the Project
Creating the App
The first step you’ll do is to create the project using create-react-app
, a simple command-line tool that let's you create React applications without any configuration overhead.
Open a terminal window and type the following:
Creating the Server
On the backend, you’ll use a GraphQL API provided by Graphcool. As with create-react-app, you use the Graphcool CLI to generate it:
Note: If you're looking to build your own GraphQL servers for production use cases that go beyong prototyping and learning, be sure to check out Prisma.
The remote schema file that you’re using here contains the following data model (written in GraphQL SDL syntax):
This is what it should look like in your terminal:
Note that you can now manage this project in the Graphcool Console. If you want to manage it locally, you can use the project file project.graphcool
to make local changes to the schema and then apply them by calling graphcool push
.
2. Connecting with Relay
The next step is to connect your React app with the Relay API on the server. You’ll take the following steps to achieve this:
- Add Relay dependencies
- Eject from
create-react-app
to configure Babel - Configure the Relay Environment
Let’s jump right in!
1. Add Relay Dependencies
You first have to install several dependencies to pull in the different pieces that are required for Relay to work.
In the terminal window, first install the general react-relay
package that was recently upgraded to version 1.0:
This dependency allows you to access all major Relay APIs, such as the QueryRenderer
or FragmentContainer
that you'll explore in a bit!
Next you need the two dependencies that make for much of the performance benefits in the Relay architecture through ahead-of-time optimizations: The relay-compiler
and babel-plugin-relay
. Both are installed as dev dependencies using the --dev
option:
All right, that’s it for the first step! Go ahead and move on to configure Babel.
2. Eject from create-react-app to configure Babel
create-react-app
hides all the build tooling configurations from you and provides a comfortable spot for starting out. However, in your case you actually need to do some custom Babel configurations to get Relay to work. So you need to eject from create-react-app
.
In the terminal, use the following command:
This will change the folder structure to look as follows:
This command essentially opens up the blackbox that was handed to you by create-react-app
and let's you do the build configuration yourself.
In this case, you need to add the babel-plugin-relay
that you installed in the previous step to the build process. Open package.json
and add the relay plugin by modifying the babel
section like so:
That’s it already for the Babel configuration. Set up the Relay Environmnent in the app next!
3. Configure the Relay Environment
The Relay Environment provides the core of the Relay functionality at runtime by “[bundling] together the configuration, cache storage, and network-handling that Relay needs in order to operate.”
A Relay Environment needs to be instantiated with two major components:
- A
Network
that knows which GraphQL server it can talk to - A
Store
that takes care of the caching
To achieve this, create new file in the project’s src
directory called Environment.js
and add the following code to it:
This code has been taken from the example in the docs and was only slightly customised.
Let’s quickly discuss the commented sections to understand better what’s going on:
-
You first import the required JS modules that you need to instantiate and configure the
Environment
. -
Here you instantiate the required
Store
that will store the cached data. -
Now you create a
Network
that knows your GraphQL server from before, it's instantiated with a function that returns aPromise
of a networking call to the GraphQL API - here that's done usingfetch
. -
At this point, you need to replace
__RELAY_API_ENDPOINT__
with your endpoint for the Relay API -
With the
store
andnetwork
available you can instantiate the actualEnvironment
. -
Lastly you need to export the
environment
from this module.
Awesome, you’re now ready to use Relay in your app 🚀
Note: If you lose the endpoint for your GraphQL API, you can always find it in the Graphcool Console (by clicking in ENDPOINTS-button on the bottom-left) or use the graphcool endpoints command in the same directory where the project file project.graphcool is located:
3. Displaying all Posts
Preparing React Components
Before doing anything else, go ahead and prepare the React components.
You’ll use Tachyons to ease working with CSS in this project. Open public/index.html
and add a third link tag to in the head
section so that it looks like this:
First create a new file called Post.js
in the src directory that will represent an individual post. Paste the following code into the empty file:
That’s a simple Post component that displays the image and the description for each post. You'll implement the _handleDelete
method in a bit.
Next, add another file, again in the src
directory and call it ListPage.js
. Implement it as follows:
This ListPage
component simply renders a list of Post
components by mapping over an array of posts. For now these are posts that you define statically in the mockPostDataarray, but you'll soon replace that to fetch the actual posts from the server!
To finish up this section, open App.js
and replace its contents with the following:
The App
is the root component for your application, so you tell it to render the ListPage
that will be the initial screen of the app.
With this setup, you can finally run the app. Type the following in the terminal:
This will open up a browser and load the app from http://localhost:3000
where you'll now see two lovely pigs:
Load Data from Server
As lovely as these pigs are, they’re only loaded from memory instead of the network which definitely wasn’t the goal of this exercise. Instead, you want to store the posts in the database on the server and then load them using GraphQL and Relay!
Before you go and make the required changes, a bit of theory!
Colocation and GraphQL Fragments
One of the most powerful concepts of Relay is called colocation. This means that a React component declares its data dependencies right next to (i.e. in the same file) where it’s defined. This happens in the form of GraphQL fragments.
This effectively means that you’ll never write any actual GraphQL queries yourself. This is unlike the approach that’s taken in Apollo, where you’re also able to coloate data dependencies and React components — but are most commonly doing so by writing actual queries instead of fragments.
But if you’re never writing any queries in Relay, how can the GraphQL server respond with sensible data?
That’s the cool part about Relay! Under the hood, it will figure out the most efficient way for your React components to fetch the data that’s required for them to render, based on the data dependencies they declared in their fragments.
You don’t have to worry about fetching the data one bit — all networking and caching logic is abstracted away and you can focus on writing your React components and what data they need! Declarative data fetching ftw 😎
Fragment Containers
The way to declare the data dependencies alongside your React components is by using the FragmentContainer
API.
The function createFragmentContainer
is a higher-order component that takes in two arguments:
- A React component for which you want to declare some data dependencies
- Data dependencies written as a GraphQL fragment and wrapper using the
graphql
function
Go ahead and write the fragment containers for the two components that you added before.
Open Post.js
and add the following import to its top:
All that’s done there is importing the required Relay modules that you need to create the fragment container.
Now also adjust the export at the bottom of the file by replacing the current export default Post
statement with the following:
Note: As you’re adding the fragments now, the
relay-compiler
will throw some errors when you're running it. You'll fix these in the following steps.
Here’s where it gets interesting! Let’s examine this part step-by-step:
You’re using the createFragmentContainer
higher-order component and pass in two arguments - exactly as we said before. The first argument is simply the React component, here that's the Post
. The second argument are its data requirements in the form of a GraphQL fragment wrapped using the graphql
function. The Post
component needs access to the description
and imageUrl
of a post item. The id
is added for deleting the post later on.
One important note here is that there is a naming convention for the fragments you’re creating! Each fragment should be named according to the file and the prop that will get injected into the component:<FileName>_<propName>
In your case, the file is called Post.js
and the prop in the component should be called post
. So you end up with Post_post
for the name of the fragment.
Great work so far! Go and add the the fragment container for ListPage
as well.
Open ListPage.js
and add the same import statement to the top as before:
Then replace the export default ListPage
with the following:
Similar to the Post
component, you're passing the ListPage
component along with its data requirements into createFragmentContainer
. The ListPage
needs access to a list of posts - here you're simply asking for the last 100 posts to display. In a more sophisticated app you could implement a proper pagination approach.
Notice that you’re again following the same naming convention and name the fragment ListPage_viewer
. ListPage.js
is the name of the file and viewer
is the prop that you expect in the component.
You’re also reusing the Post_post
fragment that you wrote in Post.js
. That's because the ListPage
is higher in the React component (and Relay container) tree, so it's responsible to include all the fragments of its children!
The @connection
directive is required for updating the cache later on - you need it so that you can refer to that particular connection (identified by the key ListPage_allPosts
) in the cache.
Finally, you also need to delete the mock data you used to render the posts before. Then update the part in render
where you're mapping over all post items and create the Post
components:
Rendering Queries
Now it starts to get interesting! What happens with these fragments? When are they used and what’s the query Relay actually sends to the server?
Meet the QueryRenderer
: QueryRenderer
is the root of a Relay tree. It takes a query, fetches the data and calls the render
callback with the data.
So, here is where it all adds up. React components are wrapped with GraphQL fragments to become Relay containers. When doing so, they retain the same hierarchical structure as the pure React components and form a tree. At the root of that tree there’s the QueryRenderer
, which also is a higher-order component that will take care of composing the actual query.
So, go and add the QueryRenderer
!
Open App.js
and add the following import to the top:
A QueryRenderer
needs at least three things when being instantiated:
- A Relay
environment
which is why you're importing it here. - A root query
which
will be the basis for the query that gets sent to the server. - A
render
function that specifies what should be rendered in loading, error and success cases.
You’ll write the root query
first. Add the following code between the import statements and the App
component:
Notice how we’re now actually using the fragment ListPage_viewer
from the ListPage
component.
Now reimplement render
as follows:
That’s it! The app is now connected with the GraphQL server and ready to load some lovely pigs! 🐷🍦
Running the App
If you’re just running the app now, you’ll be disappointed that it throws some errors:
That’s because we’ve skipped the compilation of the GraphQL code that makes for much of Relay’s actual power! You already installed the relay-compiler
, so now you'll actually use it.
The compiler can be invoked using the relay-compiler
command in the terminal where you have to provide two arguments:
--src
: The path to all your files that containgraphql
code--schema
: The path to your full GraphQL schema
You can get access to the full GraphQL schema by using a command line utility called get-graphql-schema
:
Note:
get-graphql-schema
has been deprecated in favor of thegraphql get-schema
command from the GraphQL CLI.
Again, you need to replace the placeholder __RELAY_API_ENDPOINT__
with the actual endpoint of your Relay API. This command then downloads the schema and saves it in a file called schema.graphql
.
ATTENTION: There’s currently a bug in the Relay Compiler that will produce an error if you download your schema like this. Until the bug is fixed, simply copy the schema from here and put it into your project in a file called
schema.graphql
. The file needs to be on the same level as thesrc
directory - not inside!
Now you can run the compiler:
The relay-compiler
will now scan all files in src
and look for graphql
code. It then takes this code and generates corresponding JavaScript representations for it (which again will be the input for the Babel compilation step). These JavaScript representations are stored in ./src/__generated__
.
Here’s what the output of the relay-compiler
looks like in the terminal:
You’ll also notice that the __generated__
directory was now created and contains all the files that were generated by the compiler:
Before you run the app to see if everything works, you should add actual post items to the database. Open a GraphQL Playground by pasting your endpoint for the Relay API into the address bar of a browser.
Once the Playground has opened, paste the following two mutations into the left pane:
Notice that you’ll have to switch the Playground mode from Simple to Relay for the mutations to work!
Then click the Play-button and select each of these mutations exactly once:
Note: You can also use the Data Browser to add some post items.
All right, you now populated the database with some initial data.
Go ahead and run yarn start
to see what the app currently looks like - you should now see the same two lovely pigs that you used as mock data before!
By the way, if you’re curios to see what the actual query looked like that the QueryRenderer
composed for you and that was sent over to the server, you can inspect the Networking-tab of your browser's dev tools:
4. Adding and Deleting Posts
You’re done with the first part of the tutorial where we wanted to load and display the posts returned by the server.
Now you need to make sure that your users can also add new posts and delete existing ones!
For adding new posts, you’ll use a new page in the app. Create a new file in the src
directory, call it CreatePage.js
and add the following code:
This is a simple view with two input
elements where the user can type the description
and imageUrl
of the post she's creating. You also display a preview of the image when an imageUrl
is available. The confirm button
is displayed only when the user provides the required info, and clicking it will invoke the _handlePost
method.
Subproblem: Routing in Relay
One thing you’ll have to figure out next is how to display that new page in the app — i.e. you need some kind of routing solution.
An interesting side-note is that Relay actually started out as a routing framework that eventually also got connected with data loading responsibilities. This was particularly visible in the design of Relay Classic, where Relay.Route
was a core component. However with Relay Modern, the idea is to move away from having routing as an integral part of Relay and make it more flexible for different routing solutions.
Since we’re in the early days of Relay Modern, there’s not really much advise or conventions to build upon. The FB team delivers a few suggestions how this can be handled. But it will certainly take some time until best practices and appropriate tools around this topic evolve!
So, to keep it simple in this tutorial, we’ll use react-router
which is a popular routing solution. The first thing you need to do is install the corresponding dependency:
Then replace all contents in index.js
with the following:
Next, open ListPage.js
and add a Link
to the new page by again replacing the current implementation or render
with the following:
Also don’t forget to import the Link
component on top of the same file:
Pressing the Link
element in the app will now trigger the CreatePage
to appear on the screen. You can run the app again and you should see everything as before, plus the + New Post-button on the top right. Press it to convince yourself that it actually displays the CreatePage
component:
Creating new Posts
Now that you’ve got the routing set up, you can take care of the mutation. Mutations were one of the major pain points developers had with Relay Classic. The way how they’ve been implemented was in a declarative and powerful way. However, it was very difficult to actually understand how they worked since there was so much magic going on behind the scenes. As a result, the main concern was that they’re not predictible enough and developers had a hard time to reason about them.
That’s why one of the major goals of Relay Modern was also to introduce a new and more approachable mutation API. The Facebook team delivered that and Relay now exposes a more imperative API that allows to manipulate the local store directly (actually, the manipulation happens through a dedicated proxy object, but it’s definitely much more direct than before).
To implement the mutation for adding new posts, create a new file called CreatePostMutation.js
in src
and paste the following code into it:
Let’s quickly walk through the different things that happen here:
- First you need to import the right modules from
react-relay
as well as theenvironment
. - Here you write a simple mutation and tag it with the
graphql
function. - The module exports a single function that takes in the post’s
description
,imageUrl
, theviewerId
and acallback
that will be called when the mutation is completed. - Here you prepare the
input
object for the mutation that wraps thedescription
andimageUrl
. Note that theclientMutationId
is required in this case because of a minor limitation in the Graphcool API - it has no function. - The
commitMutation
function can be used to send a mutation to the server with Relay Modern. You're passing the information that you prepared in the previous steps and execute thecallback
once the mutation is ready. - The
optimisticUpdater
andupdater
functions are part of the new imperative mutation API that allows to manipulate the Relay store through a proxy object. We'll discuss this in more detail in a bit. - Once the mutation is fully completed, the callback that the caller passed in is invoked.
Using Relay’s New Imperative Store API
Let’s quickly discuss the optimisticUpdater
and updater
functions that are teased here. The proxyStore
that's being passed into them allows you to directly manipulate the cache with the changes you expect to happen through this mutation.
optimisticUpdater
is triggered right after the mutation is sent (before the server response comes back) - it allows you to implement the success scenario of the mutation so that the user sees the effect of her mutation right away without having to wait for the server response.
updater
is triggered when the actual server response comes back. If optimisticUpdater
is implemented, then any changes that were introduced through it will be rolled back before updater is executed.
Here is how you implement them:
Note that this code requires you to add a global variable called tempID
into the file as well:
Phew! There’s a lot of stuff going on, let’s tear it apart a bit. First notice that the second part of the both functions are completely identical! That’s because the proxyStore
(your interface to manipulate the cache) doesn't care where the object that you're inserting comes from!
So, in optimisticUpdater
, you're simply creating the newPost
yourself based on the data (description
and imageUrl
) that is provided. However, for the id
, you need to generate a new value for every post that's created and that will be the temporary ID of the post in the store until the actual one arrives from the server - that's why you introduce this tempID
variable that gets incremented with every new post.
For the updater
you can make use of the actual server response to update the cache. With getRootField
and getLinkedRecord
you get access to the payload of the mutation that you specified on top of the file:
Next you need to actually use this mutation in CreatePage.js
. The only problem left right now is that in CreatePage
, you don't have access to the viewerId
at the moment - but it's a required argument for the mutation. At this point, you could use react-router
and simply pass the viewerId
from the ListPage
on to the CreatePage
component. However, we want to make proper use of Relay and each component should be responsible for its own data dependencies.
So, we’ll add another QueryRenderer
for the CreatePage
component where the viewerId
can be fetched. Open CreatePage.js
and update render
as follows:
With this code, you’re effectively only wrapping the previous implementation of CreatePage
in a QueryRenderer
so you can request data from the server in here as well. You still need to define the CreatePageViewerQuery
that is passed to the QueryRenderer
. Put it on top of the file right after the imports:
Because you’re using this query, you get access to viewer.id
in props of the component and can pass it along when the onClick
function of the button
is invoked.
You can now finally implement _handlePost
as follows:
For that code work you also need to import the required dependencies and adjust the export statement. First add the following imports on the top of the file:
And finally replace the export default CreatePage
at the bottom with the following:
Before you’re running the app, you need to invoke the relay-compiler again:
That’s it, you can now go ahead and add a new post through the UI of your app! How about these musical fellahs right here?
Deleting Posts
The last bit of functionality that’s still missing is the ability for the user to delete existing posts. Similar to the CreatePostMutation
, the first thing you need to do is setup a new file called DeletePostMutation.js
in src
and type the following code into it:
The approach you’re taking this time is very similar to the CreatePost
mutation. First you import all dependencies, then you declare the
mutation to be sent to the server and finally you export a function that takes the required arguments and calls commitMutation
.
For now, open Post.js
and implement _handleDelete
as follows:
Also don’t forget to import the mutation on the top of the file:
Once more, invoke the relay-compiler
and then run the app:
Deleting posts will now actually work, however, the UI doesn’t get updated. The posts only actually disappear after you refresh the page. Again, that’s precisely what Relay’s new imperative mutation API is for. In optimisticUpdater
and updater
you have to specify how you'd like Relay to update the cache after the mutation was performed.
Open DeletePostMutation.js
again and implement them as follows:
The optimisticUpdater
and updater
also work in the same ways as before - except that in the optimisticUpdater
you have to do less work and don't have to create a temporary mocked post object. In the updater, you're accessing the deletePost
and deletedId
fields that you specified in the selection set of the mutation.
With this code, you’re telling Relay that you’d like to remove the deleted posts (identified by deletedId
which is specified in the selection set of the mutation) from the allPosts
connection.
You need to make a few more adjustments for this to work!
First you have to pass the viewerId
as an argument when calling DeletePostMutation
in Post.js
. However, the Post
component currently doesn't have access to it (i.e. it doesn't declare it as a data dependency).
You’ll have to add another fragment to the Post
component. Open Post.js
and update the export statement as follows:
Now you can access a field called viewer
with an id
inside the props of the Post
component. Use this when you're calling DeletePostMutation
in _handleDelete
:
For this new Post_viewer
fragment to take effect, you need to also include it in the fragment container of ListPage
. Open ListPage.js
and update the export statement like so:
Now update the way how the Post
components are created in render
:
Before you run the app, you need to invoke the Relay compiler again. You can then click on the Delete-button on any post and the UI will update immediately.
Conclusion
In this tutorial you learned how to get off the ground with Relay Modern and built your own Instagram application from scratch using create-react-app
. If you got lost along the way, you can check out the final version of the code on GitHub.
Relay Modern is a great technology that is a tremendous help in building React applications at scale. Its major drawbacks right now are the still scarce documentation and unclear usage patterns and best practices, for example around routing.
We hope you enjoyed learning about Relay Modern! If you have any questions, check out our documentation or join our Slack.
To stay up-to-date about everything that happens in the GraphQL community, subscribe to GraphQL Weekly.
Don’t miss the next post!
Sign up for the Prisma Newsletter