Search code examples
reactjsgraphqlreact-apolloapollo-clientgraphiql

Errors show correctly on Graphiql but not returned to Apollo Client in react front end


I purposely left the name field blank ("") so that I can test my custom error form checking on the GraphQL backend. Using Graphiql, the errors array shows up nicely. (String! only prevents null, not "".)

Can anyone explain why errors do not get to the actual react component and what to do to fix that?

PS: The mutation works perfectly once the fields are filled. It updates with the newly created record too.

Graphiql Query pane

mutation addEmployee(
  $name: String!
  $position: String!
) {
  addEmployee(name: $name, position: $position) {
    name
    position
  }
}

query getEmployees {
  employees {
    _id
    name
    position
    createdAt
  }
}

Graphiql Query Variables: Note the empty name field.

{
  "name": "",
  "position": "CEO",
}

Graphiql Results Pane - working as expected.

{
  "errors": [
    {
      "message": "Field cannot be empty",
      "statusCode": 400
    }
  ],
  "data": {
    "addEmployee": null
  }
}

A console log on getEmployeesQuery in the react component shows this:

called: true
error: undefined
fetchMore: ƒ (fetchMoreOptions)
loading: false
networkStatus: 7
refetch: ƒ (variables)
employees: (16) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
startPolling: ƒ (pollInterval)
stopPolling: ƒ ()
subscribeToMore: ƒ (options)
updateQuery: ƒ (mapFn)
variables: {}

This is my mutation:

const Mutation = new GraphQLObjectType({
  name: "Mutation",
  fields: {
    addEmployee: {
      type: EmployeeType,
      args: {
        name: { type: new GraphQLNonNull(GraphQLString) },
        position: { type: new GraphQLNonNull(GraphQLString) },
      },
      resolve(parent, args) {
        let employee = new Employee({
          name: args.name,
          position: args.position,
        });
        let errors = [];

        try {
          if (!args.name) {
            errors.push("name");
          }
          if (errors.length) throw new Error(errorName.INVALID_FIELD);
          return employee.save();
        } catch (err) {
          throw new GraphQLError(err.message);
        }
      }
    }
  }
});

This is my component:

const Employees = ({
  getEmployeesQuery: { employees, loading, errors },
  addEmployeeMutation
}) => {

  const [state, setState] = useState({
    name: "",
    position: "",
  });

  const showEmployee = id => () => {
    const employee = store.state.employees.find(v => v._id === id);
  };

  const handleChange = name => evt => {
    setState({ ...state, [name]: evt.target.value });
  };

  const addEmployee = () => {
    addEmployeeMutation({
      variables: {
        name: state.name,
        position: state.position,
      },
      refetchQueries: [{ query: getEmployeesQuery }]
    });
  };

  return (
    <div className={styles.root}>
      <h2>Employees</h2>
      <div className={styles.listContainer}>
        <header>
          <div>Employee Name</div>
          <div>Position</div>
        </header>
        <div className={styles.list}>
          {!loading ? (
            employees.map(v => (
              <Employee
                key={v._id}
                showEmployees={showEmployees(v._id)}
                position={v.position}
                id={v._id}
              />
            ))
          ) : (
            <Loader />
          )}
        </div>
      </div>
      {(errors || []).map((error, i) => (
        <div>{error.message}</div>
      ))}
      <EmployeeForm
        fields={state}
        handleChange={handleChange}
        submit={addEmployee}
      />
    </div>
  );
};

Employees.propTypes = {
  classes: PropTypes.object,
  route: PropTypes.string,
  name: PropTypes.string
};

export default compose(
  getEmployeesQuery,
  addEmployeeMutation
)(Employees);

The queries:

import { gql } from "apollo-boost";
import { graphql } from "react-apollo";

export const getEmployeesQuery = graphql(
  gql`
    {
      employees {
        _id
        createdAt
        name
        position
      }
    }
  `,
  { name: "getEmployeesQuery" }
);

export const addEmployeeMutation = graphql(
  gql`
    mutation(
      $name: String!
      $position: String!
    ) {
      addEmployee(
        name: $name
        position: $position
      ) {
        _id
        createdAt
        name
        position
      }
    }
  `,
  { name: "addEmployeeMutation" }
);

This is getting long but it's the last one. Promise. Here's the index.js!

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";

import "./index.css";
import App from "./layout/App";
import * as serviceWorker from "./serviceWorker";

import { ApolloProvider } from "react-apollo";
import { ApolloClient } from "apollo-client";
import { createHttpLink } from "apollo-link-http";
import { InMemoryCache } from "apollo-cache-inmemory";

const httpLink = createHttpLink({
  uri: "http://localhost:4000/graphql"
});

const client = new ApolloClient({
  link: httpLink,
  cache: new InMemoryCache()
});

ReactDOM.render(
  <ApolloProvider client={client}>
      <BrowserRouter>
        <App />
      </BrowserRouter>
  </ApolloProvider>,
  document.getElementById("root")
);

serviceWorker.register();

Solution

  • The problem was elementary. But for those who run into the same silly trap . . . Make sure you run preventDefault() on the submit event. The default action reloaded the page on submit which only ran getEmployeesQuery. This is why nothing existed on addEmployeeMutation. preventDefault forces addEmployeeMutation to return with the errors.

    Also make sure to catch the errors in a try/catch. They are not included in the props. (Thank you Long Nguyen).

    const [errors, setErrors] = useState([]);
    
    const addEmployee = async evt => {
        evt.preventDefault(); // This fixed it!
        try {
          await addEmployeeMutation({
            variables: {
              name: state.name,
              position: state.position,
            },
            refetchQueries: [{ query: queries.getEmployeesQuery }]
          });
        } catch (e) {
          setErrors([e.message]);
        }
      };