Apollo Client Write Query Not Updating UI

11.3k views Asked by At

We are building an offline first React Native Application with Apollo Client. Currently I am trying to update the Apollo Cache directly when offline to update the UI optimistically. Since we offline we do not attempt to fire the mutation until connect is "Online" but would like the UI to reflect these changes prior to the mutation being fired while still offline. We are using the readQuery / writeQuery API functions from http://dev.apollodata.com/core/read-and-write.html#writequery-and-writefragment. and are able to view the cache being updated via Reacotron, however, the UI does not update with the result of this cache update.

    const newItemQuantity = existingItemQty + 1;
    const data = this.props.client.readQuery({ query: getCart, variables: { referenceNumber: this.props.activeCartId } });
    data.cart.items[itemIndex].quantity = newItemQuantity;
    this.props.client.writeQuery({ query: getCart, data });
2

There are 2 answers

4
Sebastien Lorber On

If you look at the documentation examples, you will see that they use the data in an immutable way. The data attribute passed to the write query is not the same object as the one that is read. Mutating this object is unlikely to be supported by Apollo because it would not be very efficient for it to detect which attributes you modified, without doing deep copies and comparisons of data before/after.

const query = gql`
  query MyTodoAppQuery {
    todos {
      id
      text
      completed
    }
  }
`;
const data = client.readQuery({ query });
const myNewTodo = {
  id: '6',
  text: 'Start using Apollo Client.',
  completed: false,
};
client.writeQuery({
  query,
  data: {
    todos: [...data.todos, myNewTodo],
  },
});

So you should try the same code without mutating the data. You can use for example set of lodash/fp to help you

const data = client.readQuery({...});
const newData = set("cart.items["+itemIndex+"].quantity",newItemQuantity,data);
this.props.client.writeQuery({ ..., data: newData });

It recommend ImmerJS for more complex mutations

0
ronatory On

Just to save someones time. Using the data in an immutable way was the solution. Agree totally with this answer, but for me I did something else wrong and will show it here. I followed this tutorial and updating the cache worked fine as I finished the tutorial. So I tried to apply the knowledge in my own app, but there the update didn’t work even I did everything similar as showed in the tutorial.

Here was my approach to update the data using the state to access it in the render method:

// ... imports

export const GET_POSTS = gql`
    query getPosts {
        posts {
            id
            title
        }
     }
 `

class PostList extends Component {

    constructor(props) {
        super(props)

        this.state = {
            posts: props.posts
        }
    }

    render() {    
        const postItems = this.state.posts.map(item => <PostItem key={item.id} post={item} />)

        return (
            <div className="post-list">
                {postItems}
            </div>
        )
    }

}

const PostListQuery = () => {
    return (
        <Query query={GET_POSTS}>
            {({ loading, error, data }) => {
                if (loading) {
                    return (<div>Loading...</div>)
                }
                if (error) {
                    console.error(error)
                }

                return (<PostList posts={data.posts} />)
            }}
        </Query>
    )
}

export default PostListQuery

The solution was just to access the date directly and not using the state at all. See here:

class PostList extends Component {

    render() {
        // use posts directly here in render to make `cache.writeQuery` work. Don't set it via state
        const { posts } = this.props

        const postItems = posts.map(item => <PostItem key={item.id} post={item} />)

        return (
            <div className="post-list">
                {postItems}
            </div>
        )
    }

}

Just for completeness here is the input I used to add a new post and update the cache:

import React, { useState, useRef } from 'react'
import gql from 'graphql-tag'
import { Mutation } from 'react-apollo'
import { GET_POSTS } from './PostList'

const ADD_POST = gql`
mutation ($post: String!) {
  insert_posts(objects:{title: $post}) {
    affected_rows 
    returning {
      id 
      title
    }
  }
}
`

const PostInput = () => {
  const input = useRef(null)

  const [postInput, setPostInput] = useState('')

  const updateCache = (cache, {data}) => {
    // Fetch the posts from the cache 
    const existingPosts = cache.readQuery({
      query: GET_POSTS
    })

    // Add the new post to the cache 
    const newPost = data.insert_posts.returning[0]

    // Use writeQuery to update the cache and update ui
    cache.writeQuery({
      query: GET_POSTS,
      data: {
        posts: [
          newPost, ...existingPosts.posts
        ]
      }
    })

  }

  const resetInput = () => {
    setPostInput('')
    input.current.focus()
  }

  return (
    <Mutation mutation={ADD_POST} update={updateCache} onCompleted={resetInput}>
      {(addPost, { loading, data }) => {
        return (
          <form onSubmit={(e) => {
            e.preventDefault()
            addPost({variables: { post: postInput }})
          }}>
            <input 
              value={postInput}
              placeholder="Enter a new post"              
              disabled={loading}
              ref={input}
              onChange={e => (setPostInput(e.target.value))}              
            />
          </form>
        )
      }}
    </Mutation>
  )
}

export default PostInput