π» Frontend
Learn how apollo client manages asynchronous state through useMutation
date
Jun 29, 2022
slug
apollo-gettings-started-mutate
author
status
Public
tags
Next.js
GraphQL
Apollo
summary
Let's try useMutation and see how it handles real asynchronous state.
type
Post
thumbnail
category
π» Frontend
updatedAt
Feb 23, 2023 12:35 PM
Getting Started
In the previous article, we looked up data using the
useQuery
hook, so letβs handle mutation through useMutation
this time.Set up your development environment
It would be good to refer to the previous article about setting up the development environment. This article assumes that the development environment setup has been completed, and continues with the previous article.
useMutation
When mutation is passed to the hook, the state value for the corresponding result value is delivered like mutateFunction and
useQuery
that actually request mutate. You can request mutation through the corresponding mutateFunction.The example below is a component that requests a mutation that creates an item using
useMutation
.//..... const initialInputs = { title: '', description: '', } const CREATE_ITEM = gql` mutation CreateItem($input: CreateItemInput!) { createItem(input: $input) { id title description compleated } } ` function CreateForm({ data }: Props) { const classes = useStyles() const [createItem] = useMutation(CREATE_ITEM) const [inputs, setInputs] = useState(initialInputs) const handleChange: ChangeEventHandler< HTMLTextAreaElement | HTMLInputElement > = (e) => { const { name, value } = e.target setInputs({ ...inputs, [name]: value, }) } const handleOnSubmit: FormEventHandler<HTMLFormElement> = async (e) => { e.preventDefault() try { if (inputs.title && inputs.description && data?.list?.id) { createItem({ variables: { input: { listId: listId, ...inputs, }, }, refetchQueries: [GetTodoListDocument], }) } catch (error) { console.log(error) } } return ( <form className={classes.wrapper} onSubmit={handleOnSubmit}> <h1>Add todo</h1> <TextField name="title" value={inputs.title} onChange={handleChange} label="Title" variant="outlined" /> <TextField name="description" value={inputs.description} onChange={handleChange} label="Description" multiline minRows={10} maxRows={10} variant="outlined" /> <Button variant="contained" type="submit"> add </Button> </form> ) } export default CreateForm
Updating data cached locally
When using
useMutation
, there is one thing to consider differently from useQuery
. The point is that when a request to modify back-end data is made through mutation, the previously cached data must also be modified to match the changed data in the actual back-end. (Otherwise, you will have to retrieve the data again through refresh to see the modified part.)If previously cached data is modified (update)
In the case of modifying existing cached data, if there is
__typename
and id
in the response data when requesting mutation, the corresponding data is automatically modified. (This part felt more comfortable than having to implement it yourself in redux.)When data that is not cached is modified (create, delete)
As mentioned earlier,
useMutation
only requests mutation to the server, so it is necessary to modify the cached data because it does not know how to update the cached state that remains locally.Updating the cache using refetching queries
Among the methods of updating cached data, the most common method is to receive the updated data again through communication to inquire the data. In the apollo client, this can be implemented through
refetchingQueries
. Let's look only at the part that calls createItem
in the code above.//... const GetTodoListDocument = gql` query getTodoList($listId: ID!) { list(id: $listId) { id title items { id title description compleated } } } ` createItem({ variables: { input: { listId: listId, ...inputs, }, }, refetchQueries: [GetTodoListDocument], }) //...
If you pass the query to the
refetchQueries
property as follows, you can update the cached data by executing the query after the mutation is normally performed.Updating the cache directly through the update
function
If you proceed in the above way, one more communication process will occur in addition to mutation. Using the update function, it is also possible to directly modify the cache according to the changed result, depending on whether or not the mutation has responded. Let's check the example below.
createItem({ variables: { input: { listId: listId, ...inputs, }, }, update(cache, result) { cache.modify({ id: cache.identify(data?.list as StoreObject), fields: { items(cachedItemRefs: StoreObject[], { readField }) { const newItemRef = cache.writeFragment({ data: result.data?.createItem, fragment: gql` fragment NewItem on Item { id title description compleated } `, }) if ( cachedItemRefs.some( (ref) => readField('id', ref) === result.data?.createItem?.id ) ) { return cachedItemRefs } return [...cachedItemRefs, newItemRef] }, }, }) }, })
To the update function, we are passing the InMemeryCache instance that we put in when creating the apollo client instance and the result value. You can use the relevant factor to read the api document and modify it appropriately (?).
Renew ahead of time via OptimisticResponse
If you use the above two methods, the cache is changed only after the communication is over, so it may feel like it is reflected slowly from the user's point of view. Therefore, the form of reflecting the state value that changes in advance in the expected form, leaving it as it is if the mutation was actually performed normally, and returning it if it failed is called Optimistic UI. In the apollo client, this can be easily implemented through the
OptimisticResponse
property.createItem({ variables: { input: { listId: data?.list?.id, ...inputs, }, }, optimisticResponse: { createItem: { id: 'id-temp', __typename: 'Item', title: inputs.title, description: inputs.description, compleated: false, }, }, update(cache, result) { cache.modify({ id: cache.identify(data?.list as StoreObject), fields: { items(cachedItemRefs: StoreObject[], { readField }) { const newItemRef = cache.writeFragment({ data: result.data?.createItem, fragment: gql` fragment NewItem on Item { id title description compleated } `, }) if ( cachedItemRefs.some( (ref) => readField('id', ref) === result.data?.createItem?.id ) ) { return cachedItemRefs } return [...cachedItemRefs, newItemRef] }, }, }) }, })
If the actual
update
function is applied in the same way and the expected result value is written in the optimisticResponse
property, the cache is updated with the expected result value in advance before the response is made.Concluding
In fact, no matter what communication (REST API, GraphQL) or any state management library (Redux, Recoil, React-query, Apollo, etc.) is used, if you have a clear understanding of asynchronous state management, you can quickly understand and apply it. same. Among them, we are just managing state using apollo client among communication using GraphQL... I think it's important to always try to understand the essence and become a developer who can use the technology that suits the situation.