/ react

GraphQL Mutations and Form Errors

GraphQL has a mechanism for errors. It looks like this:

{
  "data": {
    "myMutation": null
  },
  "errors": [
    {
      "message": "Name is invalid",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "myMutation.name"
      ]
    }
  ]
}

The error messages are more developer focused. Showing them directly to the user would be confusing.

If we have a form like the following:

Using the build in GraphQL errors for this form has the following drawbacks:

  • some fields might have more than one error
  • separating between input validation error and general error
  • mapping form fields with errors

It is a lot easier to handle errors as regular fields. In Product Hunt all mutations have the following format:

# `$input` is part of Relay specification
mutation MyMutation($input: MyMutationInput!) {  
  # remaping to `response` is optional, just for handling with forms
  response: myMutation(input: $input) {      
    # `node` is the record affected by mutation
    node {    
       ...dataWeCareFromResponse
    }    
    # array of validation errors
    errors {    
      name  
      messages
    }
  }
}

The SDL for the mutation is:

type Mutation {
  myMutation(input: MyMutationInput!): MyMutationPayload!
}

type Error {
  field: String!
  messages: [String!]!
}

input MyMutationInput {
  # ...inputs
}

type MyMutationPayload {
  node: # ... affected record
  errors: [Error]!
}

Since the format of all Form mutations is known. Building generic tooling around it is straightforward.

The following is the code Form used in Product Hunt.

class Form extends React.Component {    
  render() {    
    return (  
      <form onSubmit={this.handleSubmit}>      
        {this.props.children}
      </form>
    );
  }  

  handleSubmit = async (formData) => {      
    // ... guards and setup
    
    const { response: { node, errors } } = await this.props.submit(formData);
    
    if (errors.length > 0) {    
      this.setState({ errors: normalizeErrors(errors) });
    } else {
      this.props.onSuccess(node);
    }  
      
    // ... clean up
  };  
    
  // ... more form code
}