What is the Relay Store and How to Access/Update the Data in it? (In-Depth Explanation)
Yaroslav Kukytsyak
Updated Feb 28, 2021
This is an in-depth explanation of the Relay store, covering the details of how the data is actually stored as well as how to access/update that data.
What is the Relay store?
The Relay store is where Relay stores the data that has been fetched so far (though not necessarily all the fetched data, as Relay might remove data that is no longer used by the application).
The data is stored in a normalized way. It's effectively a mapping from IDs to records, where each ID is globally unique.
Each record in the store corresponds to some object (e.g., a user, a post, a comments connection, etc.).
Whenever a record needs to reference another record (e.g., a post referencing its author), this is done via the record ID. This means that we have only 1 record corresponding to a specific user, so if we update any field in it, all the records that reference this record already reference the latest data. Thus, normalization ensures data consistency.
The Relay store has a "root" record, which is the entry point from which all the currently fetched data can be traversed.
The root record has an ID of client:root
and initially has no fields linking to other records.
So, at the beginning, the normalized Relay store might look like this:
{
'client:root': {
__id: 'client:root',
__typename: '__Root'
}
}
Let's say we now perform the following GraphQL query:
query PostQuery($postId: ID!) {
post(id: $postId) {
title
author {
name
}
}
}
with variables
consisting of:
{
postId: 'UHsd8j'
}
The result might look like this:
{
"data": {
"post": {
"id": "UHsd8j",
"title": "An example post",
"author": {
"id": "HnT09q",
"name": "John"
}
}
}
}
Notice that id
was automatically added by Relay to the query.
Once we get this result, Relay will add a new field to the root record, post(id:"UHsd8J")
, which references the record representing the result of the query, i.e., a post record.
The normalized store will now look like this:
{
'client:root': {
__id: 'client:root',
__typename: '__Root',
'post(id:"UHsd8J")': {
__ref: 'UHsd8J' // ID of the post record
}
},
UHsd8J: { // Post record
__id: 'UHsd8J',
__typename: 'Post',
id: 'UHsd8J',
title: 'An example post',
author: {
__ref: 'HnT09q' // ID of the user record
}
},
HnT09q: { // User record
__id: 'HnT09q',
__typename: 'User',
id: 'HnT09q',
name: 'John'
}
}
You can actually see the normalized store using the Relay Developer Tools.
Data IDs and object IDs
You may wonder, what's the difference between the __id
field and the id
field of a record?
The __id
field is what's commonly know as "data ID" and it's the globally unique ID that identifies a record.
On the other hand, id
is the field of the object as fetched from the server, e.g., the ID of the post or user that we received from the server. Objects coming from the server that implement the Node
interface should have globally unique IDs. (See Global Object Identification.)
When Relay creates records for objects that implement the Node
interface, it makes the data ID (i.e., the record ID) match the id
field since it's known to be globally unique. So, in the example above, the __id
and id
are the same for the user record as well as for the post record.
Not all objects can implement the Node
interface and have an id
. For example, a connection or an edge of a connection does not have an id
. Nonetheless, the record created for it must have a globally unique data ID. In that case, Relay will generate a globally unique ID, which it assigns to __id
. All IDs generated by Relay are prefixed with client:
.
To recap, each record in the store must have a globally unique ID that identifies it. This ID is generally referred to as "data ID" and it corresponds to the __id
field of the record. For records representing objects that implement the Node
interface and thus have a globally unique id
field, Relay will make __id
match id
. For records representing objects that don't have an id
, Relay will generate a globally unique ID and assign it to __id
.
Connections
Let's see what happens to our normalized store when we query a connection field.
Say we now perform another query, this time asking for the post's first 2 comments:
query PostCommentsQuery($postId: ID!) {
post(id: $postId) {
comments(first: 2) {
edges {
node {
id
body
}
}
}
}
}
The result could look like this:
{
"data": {
"post": {
"id": "UHsd8j",
"comments": {
"edges": [
{
"node": {
"id": "QW5zd2",
"body": "Nice post!"
}
},
{
"node": {
"id": "Okt0qU",
"body": "Great!"
}
}
]
}
}
}
}
The normalized store will now look like this:
{
'client:root': {
__id: 'client:root',
__typename: '__Root',
'post(id:"UHsd8J")': {
__ref: 'UHsd8J' // ID of the post record
}
},
UHsd8J: { // Post record
__id: 'UHsd8J',
__typename: 'Post',
id: 'UHsd8J',
title: 'An example post',
author: {
__ref: 'HnT09q' // ID of the user record
},
// This new field has been added
'comments(first:2)': {
// ID of the comments connection record
__ref: 'client:UHsd8J:comments(first:2)'
}
},
// Record for the comments connection
'client:UHsd8J:comments(first:2)': {
__id: 'client:UHsd8J:comments(first:2)',
__typename: 'CommentConnection',
edges: {
__refs: [
// ID of the record for the first edge
'client:UHsd8J:comments(first:2):edges:0',
// ID of the record for the second edge
'client:UHsd8J:comments(first:2):edges:1'
]
}
},
// First edge
'client:UHsd8J:comments(first:2):edges:0': {
__id: 'client:UHsd8J:comments(first:2):edges:0',
__typename: 'CommentEdge',
node: {
// ID of the first comment record
__ref: 'QW5zd2',
}
},
// First comment record
QW5zd2: {
__id: 'QW5zd2',
__typename: 'Comment',
id: 'QW5zd2',
body: 'Nice post!',
},
// Second edge
'client:UHsd8J:comments(first:2):edges:1': {
__id: 'client:UHsd8J:comments(first:2):edges:1',
__typename: 'CommentEdge',
node: {
// ID of the second comment record
__ref: 'Okt0qU',
}
},
// Second comment record
Okt0qU: {
__id: 'Okt0qU',
__typename: 'Comment',
id: 'Okt0qU',
body: 'Great!',
},
HnT09q: {
// User record...
},
}
Notice how Relay automatically generated the data IDs for the comments connection and its edges in a deterministic way.
The store proxy and record proxies
Let's say we have access to the raw Relay store via a rawStore
variable and we want to get the post record, whose data ID we know to be UHsd8J
.
Since the store is a normalized mapping from IDs to records, we could access a record by its data ID like this:
const postRecord = rawStore['UHsd8J'];
We know that the post record looks like this:
{
__id: 'UHsd8J',
__typename: 'Post',
id: 'UHsd8J',
title: 'An example post',
author: {
__ref: 'HnT09q'
},
// ...
}
If we want to get the title, we just do postRecord.title
.
However, if we want to access the author record, we would need to get the author data ID from the ref object and then access it from the rawStore
:
const authorId = postRecord.author.__ref;
const authorRecord = rawStore[authorId];
This isn't ideal.
And if we wanted to change the author of the post, we would need to manually update the ref to match the data ID of the new author:
postRecord.author.__ref = newAuthor.__id;
This isn't ideal either.
So, as you can see, dealing with plain store records isn't very convenient.
This is one of the reasons why in Relay we have record proxies.
Record proxies provide an indirect way of accessing records, hiding away the complexities we just saw.
In order to access records via proxies, we first need to access the store via a proxy. The store proxy let's us access record proxies by their data IDs via the get
method.
Let's say that storeProxy
is the store proxy. We can access the post record proxy like this:
const postRecordProxy = storeProxy.get('UHsd8J');
If we want to get the post title, we would use the getValue
method on the record proxy:
const title = postRecordProxy.getValue('title');
// title === 'An example post'
If we want to get the record proxy for the post's author, we would use the getLinkedRecord
method:
const authorRecordProxy = postRecordProxy.getLinkedRecord('author');
If we want to get the record proxies for the comment edges, we would use the getLinkedRecords
method:
const commentsConnectionRecordProxy =
postRecordProxy.getLinkedRecord('comments(first:2)');
const edgeRecordProxies =
commentsConnectionRecordProxy.getLinkedRecords('edges');
To recap, when we have a record proxy and we want to access a field that represents a primitive type (e.g., a string, a number, etc.), we use the getValue
method. When we want to access a field that links to a record, we use the getLinkedRecord
method. When we want to access a field that links to a list of records, we use the getLinkedRecords
method.
When working with the Relay store using the public APIs, we always work with proxies: store proxy, record proxies, etc. So, from now on, we will be referring to proxies without explicitly mentioning that they're proxies.
Example. Let's use what we learned so far to print the details of the post that's in the store.
const post = store.get('UHsd8J');
const postTitle = post.getValue('title');
console.log('Title:', postTitle);
const author = post.getLinkedRecord('author');
const authorName = author.getValue('name');
console.log('Author:', authorName);
const commentsConnection = post.getLinkedRecord('comments(first:2)');
const commentEdges = commentsConnection.getLinkedRecords('edges');
console.log(commentEdges.length, 'comments:');
for (const commentEdge of commentEdges) {
const comment = commentEdge.getLinkedRecord('node');
const commentBody = comment.getValue('body');
console.log('-', commentBody);
}
This is what the above code will print:
Title: An example post
Author: John
2 comments:
- Nice post!
- Great!
The root record
As we saw, the Relay store has a root record, which is the entry point from which all the currently fetched data can be traversed:
{
'client:root': {
__id: 'client:root',
__typename: '__Root',
'post(id:"UHsd8J")': {
__ref: 'UHsd8J'
}
},
// Other records...
}
This root record can be accessed via the getRoot
method of the store:
const root = store.getRoot();
We can then access any of its fields, like post(id:"UHsd8J")
, which links to the record representing the result of the post query, i.e., the post record.
const post = root.getLinkedRecord('post(id:"UHsd8J")');
Turns out that we can more conveniently access the post query result by specifying the arguments separately:
const post = root.getLinkedRecord('post', { id: 'UHsd8J' });
Keep in mind that post(id:"UHsd8J")
represents the combination of all the post(id:"UHsd8J")
queries done so far.
Since we first queried for the post title and author:
post(id: $postId) {
title
author {
name
}
}
with $postId
equal to UHsd8J
, and then we queried for that same post's comments:
post(id: $postId) {
comments(first: 2) {
# ...
}
}
We will have the combination of the results under the post(id:"UHsd8J")
field, i.e., a post record that has title
, author
, and comments(first:2)
.
And if we later make those same queries again, the fields under post(id:"UHsd8J")
will be updated with the latest results, i.e., the records that post(id:"UHsd8J")
links to will be updated with the latest results since the store is normalized.
Mutating records
Let's see how we can imperatively mutate the records that are in the store.
We can change a field of a record representing a primitive type via the setValue
method, which takes the value as the first argument, and the field name as the second one.
So, we can change the post's title like this:
post.setValue('New Title', 'title');
When we mutate records, the parts of the UI that are affected, re-render with the latest values.
We can change a field that links to a record via the setLinkedRecord
method.
So, we can change the post's author like this:
const newAuthor = store.get(newAuthorId);
post.setLinkedRecord(newAuthor, 'author');
Finally, if the field links to a list of records, we can change it via the setLinkedRecords
method.
So, we can add a comment to the post like this:
const newCommentEdge = store.get(newCommentEdgeDataId);
const commentsConnection = post.getLinkedRecord('comments', { first: 2 });
const commentEdges = commentsConnection.getLinkedRecords('edges');
const nextCommentEdges = [...commentEdges, newCommentEdge];
commentsConnection.setLinkedRecords(nextCommentEdges, 'edges');
Note that this simple example simply illustrates how to use setLinkedRecords
. We will later see how to update connections in practice after performing a GraphQL mutation that creates or deletes an object.
Creating new records
A new record can be created by using the store's create
method.
The create
method takes the data ID of the new record and its GraphQL type name.
The data ID that we pass to create
has to be globally unique. We can use the generateUniqueClientID
function exported by relay-runtime
to create globally unique data IDs.
So, we could create a new post record like this:
const newPost = store.create(
generateUniqueClientID(),
'Post',
);
which will look like this in the normalized Relay store:
{
'client:local:1': {
__id: 'client:local:1',
__typename: 'Post'
},
// ...
}
Notice that the new post record has no fields other than the data ID and the type name.
We can then manually add fields to it by using setValue
, setLinkedRecord
, and setLinkedRecords
.
Another possible way to add fields to it is by copying them from an existing record by using the copyFieldsFrom
method:
newPost.copyFieldsFrom(post);
copyFieldsFrom
will copy all fields except for __id
and __typename
.
Deleting records
Records can be deleted from the Relay store by using the store's delete
method.
The delete
method takes the data ID of the record to be deleted.
For example, we can delete a user record from the store like this:
store.delete(userId);
Deleting a record from the store makes the data ID of the deleted record point to null
. Therefore, if there are any references to the deleted record, they will lead to a null
value.
For example, if the normalized store looks like this:
{
UHsd8J: { // Post record
author: {
__ref: 'HnT09q' // ID of the user record
},
// ...
},
HnT09q: { // User record
// ...
},
// ...
}
and we delete the user corresponding to the post author:
store.delete('HnT09q')
Then, the store will look like this:
{
UHsd8J: { // Post record
author: {
__ref: 'HnT09q' // ID of the deleted user record
},
// ...
},
HnT09q: null, // User ID now points to null
// ...
}
This means that if we try to get the post's author, we'll get null
:
const post = store.get('UHsd8J');
post.getLinkedRecord('author'); // null
Connection records when using @connection
When using connection based pagination in Relay, we annotate the connections over which we want to paginate with a @connection
directive, specifying a unique connection key
.
Let's look at an example fragment that has a connection:
fragment PostComments_post on Post {
comments(first: $firstComments, after: $afterComment) @connection(key: "PostComments_post_comments") {
edges {
node {
id
body
}
}
}
}
and here's a query using this fragment:
query PostCommentsQuery($postId: ID!, $firstComments: Int!, $afterComment: String) {
post(id: $postId) {
...PostComments_post
}
}
Let's say that we execute this query with the following variables:
{
postId: 'UHsd8J',
firstComments: 2
}
The result we get back might look like this:
{
"data": {
"post": {
"id": "UHsd8J",
"comments": {
"edges": [
{
"node": {
"id": "QW5zd2",
"body": "Nice post!"
},
"cursor": "YXJyY8"
},
{
"node": {
"id": "Okt0qU",
"body": "Great!"
},
"cursor": "R4ms43"
}
],
"pageInfo": {
"endCursor": "R4ms43",
"hasNextPage": true
}
}
}
}
}
Notice how Relay automatically queries for extra fields like the cursor of each edge and the page info of the connection.
The Relay store will now look like this:
{
'client:root': {
'post(id:"UHsd8J")': {
__ref: 'UHsd8J' // ID of the post record
},
// ...
},
UHsd8J: { // Post record
// Two new fields have been added to the Post record:
'comments(first:2)': {
// ID of the comments connection record
__ref: 'client:UHsd8J:comments(first:2)'
},
'__PostComments_post_comments_connection': {
// ID of the overall comments connection record
__ref: 'client:UHsd8J:__PostComments_post_comments_connection'
},
// ...
},
// Record for the first 2 comments connection
'client:UHsd8J:comments(first:2)': {
edges: {
__refs: [
// IDs of the edge records
'client:UHsd8J:comments(first:2):edges:0',
'client:UHsd8J:comments(first:2):edges:1'
]
},
// ...
},
// First edge
'client:UHsd8J:comments(first:2):edges:0': {
node: {
// ID of the record for the first comment
__ref: 'QW5zd2',
},
// ...
},
// Second edge
'client:UHsd8J:comments(first:2):edges:1': {
node: {
// ID of the record for the second comment
__ref: 'Okt0qU',
},
// ...
},
// Record for the overall comments connection
'client:UHsd8J:__PostComments_post_comments_connection': {
edges: {
__refs: [
// IDs of the edge records
'client:UHsd8J:__PostComments_post_comments_connection:edges:0',
'client:UHsd8J:__PostComments_post_comments_connection:edges:1'
]
},
// ...
},
// First edge
'client:UHsd8J:__PostComments_post_comments_connection:edges:0': {
node: {
// ID of the record for the first comment
__ref: 'QW5zd2',
},
// ...
},
// Second edge
'client:UHsd8J:__PostComments_post_comments_connection:edges:1': {
node: {
// ID of the record for the second comment
__ref: 'Okt0qU',
},
// ...
}
}
Notice the extra field __PostComments_post_comments_connection
which references a connection record. This connection record will contain all the edges fetched so far for the comments connection. So, if we load the next two comments, an extra field comments(after:"R4ms43",first:2)
will be added to the post record and it will reference a connection holding the next two edges. However, the __PostComments_post_comments_connection
field will still reference the same connection to which the two new edges have been appended:
{
UHsd8J: { // Post record
'comments(first:2)': {
// ID of the connection with the first 2 comments
__ref: 'client:UHsd8J:comments(first:2)'
},
'comments(after:"R4ms43",first:2)': {
// ID of the connection with the next 2 comments
__ref: 'client:UHsd8J:comments(after:"R4ms43",first:2)'
},
'__PostComments_post_comments_connection': {
// ID of the connection with all 4 comments
__ref: 'client:UHsd8J:__PostComments_post_comments_connection'
},
// ...
},
'client:UHsd8J:comments(first:2)': {
edges: {
__refs: [
'client:UHsd8J:comments(first:2):edges:0',
'client:UHsd8J:comments(first:2):edges:1'
]
},
// ...
},
'client:UHsd8J:comments(after:"R4ms43",first:2)': {
edges: {
__refs: [
'client:UHsd8J:comments(after:"R4ms43",first:2):edges:0',
'client:UHsd8J:comments(after:"R4ms43",first:2):edges:1'
]
},
// ...
},
// Record for the overall comments connection
'client:UHsd8J:__PostComments_post_comments_connection': {
edges: {
__refs: [
'client:UHsd8J:__PostComments_post_comments_connection:edges:0',
'client:UHsd8J:__PostComments_post_comments_connection:edges:1',
// The two new edges were appended
'client:UHsd8J:__PostComments_post_comments_connection:edges:2',
'client:UHsd8J:__PostComments_post_comments_connection:edges:3'
]
},
// ...
},
// ...
}
The special connection that holds the combination of all the comment edges fetched so far is what the component using the pagination fragment receives from Relay, so it can render all the comments fetched so far.
ConnectionHandler utilities
relay-runtime
exports ConnectionHandler
which provides utilities that help to more easily access and modify connection records.
Let's say a user just created a comment and we want to update the comments connection so that the user can see their new comment.
We know that the connection rendered by our component is the special connection holding the combination of all the comment edges fetched so far. So, this is the connection to which we need to append a new edge for the new comment.
The field with this connection is __PostComments_post_comments_connection
, so we could just access it directly:
const commentsConnection =
post.getLinkedRecord('__PostComments_post_comments_connection');
Notice that the name of this field is simply a double underscore, followed by the connection key, followed by _connection
.
ConnectionHandler
has a getConnection
utility that lets us access the connection more easily given the record with the connection and the connection key:
const commentsConnection = ConnectionHandler.getConnection(
post,
'PostComments_post_comments'
);
Now that we have access to the overall comments connection, we can add an edge for the new comment.
Rather than having to create the new edge ourselves, we can use the createEdge
utility of ConnectionHandler
, which creates a new edge given the store, the connection in which the edge will be inserted, the edge node, and the GraphQL type name of the edge:
const newCommentEdge = ConnectionHandler.createEdge(
store,
commentsConnection,
newComment,
'CommentEdge'
);
which is basically equivalent to the following:
const newCommentEdgeDataId =
commentsConnection.getDataID() + ':' + newComment.getDataID();
const newCommentEdge = store.create(newCommentEdgeDataId, 'CommentEdge');
newCommentEdge.setLinkedRecord(newComment, 'node');
Then, in order to add the new edge to the connection, we could use the insertEdgeAfter
or insertEdgeBefore
utilities to insert the edge at the end or at the beginning of the connection, respectively.
Let's use insertEdgeAfter
since we want to append the new comment to the end:
ConnectionHandler.insertEdgeAfter(
commentsConnection,
newCommentEdge
);
which is basically doing this:
const commentEdges = commentsConnection.getLinkedRecords('edges');
const nextCommentEdges = [...commentEdges, newCommentEdge];
commentsConnection.setLinkedRecords(nextCommentEdges, 'edges');
On the other hand, if a user deletes a comment and we want to remove it from the connection, we can use the deleteNode
utility, to which we pass the connection and the ID of the comment to remove:
ConnectionHandler.deleteNode(
commentsConnection,
commentId
);
deleteNode
will find the edge that has the node with the specified ID and remove it from the connection. Basically doing this:
const commentEdges = commentsConnection.getLinkedRecords('edges');
const nextCommentEdges = commentEdges.filter((edge) =>
edge.getLinkedRecord('node').getDataID() !== commentId
);
commentsConnection.setLinkedRecords(nextCommentEdges, 'edges');
Updater functions
Updater functions are most commonly used to update the store after a mutation, but can also be used in other cases like making local store updates (with commitLocalUpdate
).
Everything that we learned so far puts us in a very comfortable position to write updater functions.
An updater function basically takes a proxy to the store and performs any updates it wants:
const updater = (store) => {
// ...
}
Let's look at an example of how to write a mutation updater function in practice.
So, let's say we have a mutation that creates a post comment:
const mutation = graphql`
mutation CreateComment($input: CreateCommentInput!) {
createComment(input: $input) {
comment {
...Comment_comment
}
}
}
`
where CreateCommentInput
is a GraphQL input type:
input CreateCommentInput {
"""The ID of the post to which the comment will be added."""
postId: ID!
"""The comment body."""
body: String!
}
So, our variables will need include such input:
const variables = {
input: {
postId: 'UHsd8J',
body: 'Well written!'
}
};
Notice that we also spread the Comment_comment
fragment inside the comment
field. We do this because we want to add the new comment to the comments connection, and therefore the new comment should have the fields that all the comments inside that connection have. In our case, it just happens that the component that is rendering the comments connection is spreading the Comment_comment
fragment inside the node
field of each comment edge:
fragment Comments_post on Post {
comments(first: $firstComments, after: $afterComment) @connection(key: "Comments_post_comments") {
edges {
node {
...Comment_comment
}
}
}
}
So, spreading the Comment_comment
fragment inside the new comment ensures that the comment added to the connection will have the fields that all the other comments in the connection have.
If we wanted to add the new comment to multiple connections, we should query for the combination of all the fields that comments have in all those connections, so that the comment that we will insert into each of those connection will have all the fields that are expected to be there.
Next, let's write the updater function that will append the new comment to the post comments connection after the mutation succeeds (non-null checks have been omitted for simplicity):
const updater = (store) => {
const createCommentPayload = store.getRootField('createComment');
const newComment = createCommentPayload.getLinkedRecord('comment');
const post = store.get(postId);
const commentsConnection = ConnectionHandler.getConnection(
post,
'Comments_post_comments'
);
const newCommentEdge = ConnectionHandler.createEdge(
store,
commentsConnection,
newComment,
'CommentEdge'
);
ConnectionHandler.insertEdgeAfter(
commentsConnection,
newCommentEdge
);
}
Notice that we access the record with the mutation payload using store.getRootField
. Let's see what this is about.
Once we get the result of the mutation, another root record is created that is specific to this mutation. This root record has an ID that is a unique client ID, like client:local:0
and it has a field for every root field of the mutation.
In our case, the mutation has a single root field, which is createComment
. Therefore, this is how the mutation root record will look like in the store (assuming its ID is client:local:0
):
{
'client:local:0': {
__id: 'client:local:0',
__typename: '__Root',
'createComment(input:{"body":"Well written!","postId":"UHsd8J"})': {
__ref: 'client:local:0:createComment(...)' // ID of the payload record
}
},
// ...
}
In order to access the payload record from the mutation root record, we would need to do the following:
const mutationRoot = store.get('client:local:0');
const createCommentPayload = mutationRoot.getLinkedRecord(
'createComment',
{
input: variables.input
}
);
On the other hand, by using store.getRootField
, we only need to do:
const createCommentPayload = store.getRootField('createComment');
which is much more convenient.
So, in the context of a mutation updater function, "root field" in store.getRootField
refers to a root field of the mutation. In our case the mutation has only one root field: createComment
. So, we can only access that field using this method. The advantage of using it is that we don't have to get that field from the mutation root record ourselves, having to also know its ID and specify the field arguments.
You may wonder, but how does store.getRootField
know the mutation root record ID as well as the arguments of each field? Since we just performed a mutation, Relay ties the store proxy that is passed to the updater function to the mutation's details and result, so it knows the mutation root record ID as well as the arguments of each root field of the mutation and therefore it's enough for us to only specify the field name in order to access a root field of the mutation.
store.getRootField
can only be used to get a field that is a single record. If the mutation payload was a list of objects, we would need to use store.getPluralRootField
instead. The difference between the two methods is equivalent to the difference between getLinkedRecord
and getLinkedRecords
.
Finally, we can perform the mutation using commitMutation
:
commitMutation(
relayEnvironment,
{
mutation,
variables,
updater,
onCompleted: () => console.log('Completed!'),
onError: (err) => console.error(err.message)
}
);
Conclusion
You should now have a clear understanding of how data is stored in the Relay store and how you can access and update it.
Although we covered a big part of the public API related to the Relay store, we didn't cover everything. However, you should now have the foundation that will help you understand the remaining parts of the Relay store API.