website/versioned_docs/version-v20.1.0/guided-tour/updating-data/imperatively-modifying-store-data.md
import DocsRating from '@site/src/core/DocsRating'; import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal';
:::note See also this guide on updating linked fields in the store. :::
Data in Relay stores can be imperatively modified within updater functions.
You might provide an updater function if the changes to local data are more complex than what can be achieved by simply writing a network response to the store and cannot be handled by the declarative mutation directives.
In addition, since the network response necessarily will not include data for fields defined in client schema extensions, you may wish to use an updater to initialize data defined in client schema extensions.
Lastly, there are things you can only achieve using updaters, such as invalidating nodes, deleting nodes, finding all connections at a given field, etc.
If two optimistic responses affect a given value, and the first optimistic response is rolled back, the second one will remain applied.
For example, if two optimistic responses each increase a story's like count by one, and the first optimistic response is rolled back, the second optimistic response remains applied. However, it is not recalculated, and the value of the like count will remain increased by two.
You should use the onCompleted callback to trigger other side effects. onCompleted callbacks are guaranteed to be called once, but updaters and optimistic updaters can be called repeatedly.
The useMutation and commitMutation APIs accept configuration objects which can include optimisticUpdater and updater fields. The requestSubscription and useSubscription APIs accept configuration objects which can include updater fields.
In addition, there is another API (commitLocalUpdate) which also accepts an updater function. It will be discussed in the Other APIs for modifying local data section.
Mutations can have both optimistic and regular updaters. Optimistic updaters are executed when a mutation is triggered. When that mutation completes or errors, the optimistic update is rolled back.
Regular updaters are executed when a mutation completes successfully.
Let's construct an example in which an is_new_comment field (which is defined in a schema extension) is set to true on a newly created Feedback object in a mutation updater.
# Feedback.graphql
extend type Feedback {
is_new_comment: Boolean
}
// CreateFeedback.js
import type {Environment} from 'react-relay';
import type {
FeedbackCreateData,
CreateFeedbackMutation,
CreateFeedbackMutation$data,
} from 'CreateFeedbackMutation.graphql';
const {commitMutation, graphql} = require('react-relay');
const {ConnectionHandler} = require('relay-runtime');
function commitCreateFeedbackMutation(
environment: Environment,
input: FeedbackCreateData,
) {
return commitMutation<FeedbackCreateData>(environment, {
mutation: graphql`
mutation CreateFeedbackMutation($input: FeedbackCreateData!) {
feedback_create(input: $input) {
feedback {
id
# Step 1: in the mutation response, spread an updatable fragment (defined below).
# This updatable fragment will select the fields that we want to update on this
# particular feedback object.
...CreateFeedback_updatable_feedback
}
}
}
`,
variables: {input},
// Step 2: define an updater
updater: (store: RecordSourceSelectorProxy, response: ?CreateCommentMutation$data) => {
// Step 3: Access and nullcheck the feedback object.
// Note that this could also have been achieved with the @required directive.
const feedbackRef = response?.feedback_create?.feedback;
if (feedbackRef == null) {
return;
}
// Step 3: call store.readUpdatableFragment
const {updatableData} = store.readUpdatableFragment(
// Step 4: Pass it a fragment literal, where the fragment contains the @updatable directive.
// This fragment selects the fields that you wish to update on the feedback object.
// In step 1, we spread this fragment in the query response.
graphql`
fragment CreateFeedback_updatable_feedback on Feedback @updatable {
is_new_comment
}
`,
// Step 5: Pass the fragment reference.
feedbackRef
);
// Step 6: Mutate the updatableData object!
updatableData.is_new_comment = true;
},
});
}
module.exports = {commit: commitCreateFeedbackMutation};
Let's distill what's going on here.
updater accepts two parameters: a RecordSourceSelectorProxy and an optional object that is the result of reading out the mutation response.
$data type that is imported from the generated mutation file.updater is executed after the mutation response has been written to the store.readUpdatableFragment. This returns an updatable proxy object.Let's consider the common case of updating store data in response to a user interaction. In a click handler, let's a toggle an is_selected field. This field is defined on Users in a client schema extension.
# User.graphql
extend type User {
is_selected: Boolean
}
// UserSelectToggle.react.js
import type {RecordSourceSelectorProxy} from 'react-relay';
import type {UserSelectToggle_viewer$key} from 'UserSelectToggle_viewer.graphql';
const {useRelayEnvironment, commitLocalUpdate} = require('react-relay');
function UserSelectToggle({ userId, viewerRef }: {
userId: string,
viewerRef: UserSelectToggle_viewer$key,
}) {
const viewer = useFragment(graphql`
fragment UserSelectToggle_viewer on Viewer {
user(user_id: $user_id) {
id
name
is_selected
...UserSelectToggle_updatable_user
}
}
`, viewerRef);
const environment = useRelayEnvironment();
return <button
onClick={() => {
commitLocalUpdate(
environment,
(store: RecordSourceSelectorProxy) => {
const userRef = viewer.user;
if (userRef == null) {
return;
}
const {updatableData} = store.readUpdatableFragment(
graphql`
fragment UserSelectToggle_updatable_user on User @updatable {
is_selected
}
`,
userRef
);
updatableData.is_selected = !viewer?.user?.is_selected;
}
);
}}
>
{viewer?.user?.is_selected ? 'Deselect' : 'Select'} {viewer?.user?.name}
</button>
}
Let's distill what's going on here.
commitLocalUpdate, which accepts a Relay environment and an updater function. Unlike in the previous examples, this updater does not accept a second parameter because there is no associated network payload.store.readUpdatableFragment, and toggle the is_selected field.readUpdatableFragment, this can be rewritten to use the readUpdatableQuery API.:::note
This example can be rewritten using the environment.commitPayload API, albeit without type safety.
:::
readUpdatableQuery.In the previous examples, we used an updatable fragment to access the record whose fields we want to update. This can also be possible to do with an updatable query.
If we know the path from the root (i.e. the object whose type is Query) to the record we wish to modify, we can use the readUpdatableQuery API to achieve this.
For example, we could set the viewer's name field in response to an event as follows:
// NameUpdater.react.js
function NameUpdater({ queryRef }: {
queryRef: NameUpdater_viewer$key,
}) {
const environment = useRelayEnvironment();
const data = useFragment(
graphql`
fragment NameUpdater_viewer on Viewer {
name
}
`,
queryRef
);
const [newName, setNewName] = useState(data?.viewer?.name);
const onSubmit = () => {
commitLocalUpdate(environment, store => {
const {updatableData} = store.readUpdatableQuery(
graphql`
query NameUpdaterUpdateQuery @updatable {
viewer {
name
}
}
`,
{}
);
const viewer = updatableData.viewer;
if (viewer != null) {
viewer.name = newName;
}
});
};
// etc
}
readUpdatableFragment. However, you may prefer readUpdatableQuery for several reasons:
commitLocalUpdate is not obviously associated with a component.Query in this example). Due to a known type hole in Relay, updatable fragments cannot be spread at the top level.readUpdatableFragment multiple times, each time passing different variables.