Apollo updating query with dataIdFromObject

I’m trying to find the easiest way to update a query, when I trigger a mutation. I’m not concerned about other clients or services updating the data, so subscriptions seems like an overkill.

My React apps structure don’t allow for an easy way to access each queries refetch prop, as i’m handling most mutations in a single Dialog component at root level.

Following the documentation for Apollo React, the easiest way seems to be with dataIdFromObject instead of explicit using refetch or updateQuery in every mutation of mine.

I’ve followed the exact example from: http://dev.apollodata.com/react/cache-updates.html#dataIdFromObject

import ApolloClient, {createNetworkInterface, addTypename} from 'apollo-client'

const networkInterface = createNetworkInterface('http://localhost:3000/graphql') // TBD: Need to provide the right path for production

const apolloClient = new ApolloClient({
    networkInterface: networkInterface,
    queryTransformer: addTypename,
    dataIdFromObject: (result) => {
        if (result.id && result.__typename) {
            return result.__typename + result.id
        }
        return null
    }
})

And confirmed that each result indeed has an unique id.

But even then my mutation doesn’t trigger any rerender in my query. Have I misunderstood anything on the documentation, or where am I going wrong?

My query:

const Query = gql`
  query Households {
    getHouseholds {
      id,
      address,
    }
  }
`;

My mutation:

const Query = gql`
  mutation CreateHousehold($address: String!) {
    createHousehold(address: $address) {
      id,
      address,
    }
  }
`;

Both returns the same type.

Anyone having the same issue?

I think you need to query __typename within the queries and mutation.

const Query = gql`
  mutation CreateHousehold($address: String!) {
    createHousehold(address: $address) {
      id,
      address,
      __typename
    }
  }
`;

I have similar issue. I tried using reducer with following code:

const getProjects = gql`
{
  projects {
    id
    name
  }
}
`;

export default compose(
  graphql(getProjects, {
    props: ({data: {loading, projects}}) => ({
      loading, projects,
    }),
    options({ params }) {
      return {
        reducer: (previousResult, action, variables) => {
          if (action.type === 'APOLLO_MUTATION_RESULT' && action.operationName === 'createProject') {
            return update(previousResult, {
              projects: {
                $push: [action.result.data.createProject],
              },
            });
          } else if (action.type === 'APOLLO_MUTATION_RESULT' && action.operationName === 'deleteProject') {
            const { id } = action.result.data.deleteProject;
            const index = previousResult.projects.map(p => p.id).indexOf(id);
            return update(previousResult, {
              projects: {
                $splice: [[index, 1]],
              },
            });
          }
          return previousResult;
        },
      };
    },
  }),
)(Projects);

The project is added / edited / destroyed in another component (ProjectForm) that triggers mutations like this:

class ProjectForm extends Component {
  static propTypes = {
    project: PropTypes.object,
    afterChange: React.PropTypes.func,
    createProject: React.PropTypes.func,
    updateProject: React.PropTypes.func,
    deleteProject: React.PropTypes.func,
  };

  state = { ...this.props.project };

  handleChange = (event) => {
    this.setState({[event.target.name]: event.target.value});
  };

  handleSave = (event) => {
    event.preventDefault();
    const save = this.state.id ? this.props.updateProject : this.props.createProject;
    const { id, name } = this.state;
    save({variables: { id, project: { name } }})
      .then(this.props.afterChange);
  };

  handleDelete = () => {
    this.props.deleteProject({variables: { id: this.state.id }})
      .then(this.props.afterChange);
  };

  render() {
    return (
      <div>
        <Textfield
          type="text"
          value={this.state.name}
          onChange={this.handleChange}
          label="Name"
          name="name"
          floatingLabel
        />
        <div>
          <Button raised colored ripple onClick={this.handleSave}>
            {this.state.id ? 'Update Project' : 'Create Project'}
          </Button>
        </div>
        {this.state.id && <div>
          <Button raised colored accent ripple onClick={this.handleDelete}>Delete Project</Button>
        </div>}
      </div>
    );
  }
}

const createProject = gql`
  mutation createProject($project: ProjectInput!) {
    createProject(project: $project) {
      id
      name
    }
  }
`;

const updateProject = gql`
  mutation updateProject($id: String!, $project: ProjectInput!) {
    updateProject(id: $id, project: $project) {
      id
      name
    }
  }
`;

const deleteProject = gql`
  mutation deleteProject($id: String!) {
    deleteProject(id: $id) {
      id
      name
    }
  }
`;

export default compose(
  graphql(createProject, {name : 'createProject'}),
  graphql(updateProject, {name : 'updateProject'}),
  graphql(deleteProject, {name : 'deleteProject'}),
)(ProjectForm);

This code kind of works - the projects gets elements added, updated and removed, but I see this error in console log:

Warning: flattenChildren(...): Encountered two children with the same key, `project-586a6ff8a1c3182734901892`. Child keys must be unique; when two children share a key, only the first child will be used.
    in ul (created by List)
    in List (created by ProjectsList)
    in ProjectsList (created by Projects)
    in div (created by Projects)
    in Projects (created by Apollo(Projects))
    in Apollo(Projects) (created by RouterContext)
    in div (created by Content)
    in Content (created by AppLayout)
    in div (created by Layout)
    in div (created by Layout)
    in MDLComponent (created by Layout)
    in Layout (created by AppLayout)
    in AppLayout (created by Apollo(AppLayout))
    in Apollo(AppLayout) (created by RouterContext)
    in RouterContext (created by Router)
    in Router (created by AppContainer)
    in ApolloProvider (created by AppContainer)
    in AppContainer

BTW The apolloClient creation uses simple dataIdFromObject:

export const apolloClient = new ApolloClient({
  dataIdFromObject: o => o.id,
  networkInterface: createNetworkInterface({
    uri: '/graphql',
  }),
});