Wan

Ways to Nest Mutation

June 15, 2019

Lately, I have been writing a lot of React components that takes in a render prop since that’s the pattern that is used by Apollo Client’s Mutation and Query.

As a beginner to React, using render props seemed awesome at first, but that impression doesn’t last once I started writing nested queries and mutations.

There are many reasons for my increasing uneasiness with render props, some of which includes additional layers of code in the render function, increased number of annoying curly braces to keep track of, and greater code length in a single file.

In this post, I will list alternative methods to write Apollo Client’s nested queries and mutations and briefly describe how to use them.

There’s a repo for all the examples outlined in this post: ways-to-nest-mutation. You can run the frontend and backend with npm start. Just remember to npm install both directories.

1) Nested Mutation

This is the ‘vanilla’ way of writing the Apollo Client’s Mutation and Query components.

Here’s a contrived scenario: I want to save my data and afterwards submit it. The process will utilise a client that will communicate with a graphQL server; the latter will take my queries or mutations requests, resolve them, and send it back to the client for rendering.

With nesting, the code might look like so:

import React from "react"
import { Mutation } from "react-apollo"
import gql from "graphql-tag"

const SAVE_MUTATION = gql`
  mutation SAVE_MUTATION($dataToSave: String!) {
    save(dataToSave: $dataToSave)
  }
`

const SUBMIT_MUTATION = gql`
  mutation SUBMIT_MUTATION($dataToSubmit: String!) {
    submit(dataToSubmit: $dataToSubmit)
  }
`

class DefaultNest extends React.Component {
  state = {
    input: "",
  }
  render() {
    const { input } = this.state
    return (
      <>
        <Mutation mutation={SAVE_MUTATION} variables={{ dataToSave: input }}>
          {(saveMutation, { data, error }) => {
            error && alert(error)
            data && console.log(data)
            return (
              <>
                {/* // if there is no data from the first mutation, render the button that
                    // will call the first mutation */}
                {!data && (
                  <>
                    <input
                      type="text"
                      placeholder="Enter your data here"
                      value={input}
                      onChange={e => {
                        this.setState({
                          input: e.target.value,
                        })
                      }}
                    />
                    <button onClick={saveMutation}>Save</button>
                  </>
                )}
                {/* // if there is data from the first mutation, render the button that
                    // will call the second mutation */}
                {data && (
                  <Mutation
                    mutation={SUBMIT_MUTATION}
                    variables={{ dataToSubmit: data.save }}
                  >
                    {(submitMutation, { data: submitData, error }) => (
                      <>
                        {error && console.log(error)}
                        <span>Data to submit: {data.save} </span>
                        {submitData && <p>{submitData.submit}</p>}
                        {!submitData && (
                          <button onClick={submitMutation}>Submit</button>
                        )}
                      </>
                    )}
                  </Mutation>
                )}
              </>
            )
          }}
        </Mutation>
      </>
    )
  }
}

export default DefaultNest

export { SAVE_MUTATION, SUBMIT_MUTATION }

The example is long because I used some input tags. If you can’t see the pattern, here’s a simpler version:

<Mutation>
  {(firstMutation, { data, error, loading }) => {
    // logic
    return (
      <Mutation>
        {(secondMutation, { data, error, loading }) => {
          // logic
          return // whatever
        }}
      </Mutation>
    )
  }}
</Mutation>

This is an okay pattern if you have two or maybe three components but more than that…it gets unwieldy.

2) react-adopt

From what I’ve read, react-adopt is the most recommended solution to counteract the messiness of nested render props. The idea is to use the adopt method which will take in one or multiple render props and return a component. The component, normally named Composed, will take a render prop and that render prop will have access to all arguments of the render props we previously passed to adopt.

The explanation above can be confusing thus it is a good time to see things in code. Observe this:

import React from "react"
import { Mutation } from "react-apollo"
import { adopt } from "react-adopt"
import { SAVE_MUTATION, SUBMIT_MUTATION } from "./DefaultNest"

const save = ({ render }) => (
  <Mutation mutation={SAVE_MUTATION}>
    {(saveMutation, saveResult) => render({ saveMutation, saveResult })}
  </Mutation>
)

const submit = ({ render }) => (
  <Mutation mutation={SUBMIT_MUTATION}>
    {(submitMutation, submitResult) => render({ submitMutation, submitResult })}
  </Mutation>
)

const Composed = adopt({
  save,
  submit,
})

The pattern used in save and submit is react-adopt’s own and is required to use a render props with multiple arguments. This is the case with Mutation since it has a mutation function and mutation result as arguments.

The variables save and submit can then be passed to adopt and a component will be returned.

This new component can be used in render:

class AdoptNest extends React.Component {
  state = {
    input: "",
  }

  render() {
    const { input } = this.state
    return (
      <>
        <Composed>
          {({
            save: { saveMutation, saveResult },
            submit: { submitMutation, submitResult },
          }) => {
            const { data } = saveResult
            const { data: submitData } = submitResult

            // if there is no data from the first mutation, render the button that
            // will call the first mutation
            if (!data) {
              return (
                <>
                  <input
                    type="text"
                    placeholder="Enter your data here"
                    value={input}
                    onChange={e => {
                      this.setState({
                        input: e.target.value,
                      })
                    }}
                  />
                  <button
                    onClick={() => {
                      saveMutation({
                        variables: {
                          dataToSave: input,
                        },
                      })
                    }}
                  >
                    Save
                  </button>
                </>
              )
            }
            // if there is data from the first mutation, render the button that
            // will call the second mutation
            else
              return (
                <>
                  <span>Data to submit: {data.save} </span>
                  {submitData && <p>{submitData.submit}</p>}
                  {!submitData && (
                    <button
                      onClick={() => {
                        submitMutation({
                          variables: {
                            dataToSubmit: data.save,
                          },
                        })
                      }}
                    >
                      Submit
                    </button>
                  )}
                </>
              )
          }}
        </Composed>
      </>
    )
  }
}

In Composed, we have immediate access to all the render props argument. Consequentially, we can use the mutations and results anywhere we want within the Composed’s render props.

3) graphql() and compose()

According to the react-apollo docs:

The graphql() function is the most important thing exported by react-apollo. With this function you can create higher-order components that can execute queries and update reactively based on the data in your Apollo store. The graphql() function returns a function which will “enhance” any component with reactive GraphQL capabilities. This follows the React higher-order component pattern which is also used by react-redux’s connect function.

This means that the component you pass to the function generated by graphql() will have access to whatever is returned by the queries or mutations as props. Since we want to use multiple mutations, react-apollo provides us with a compose method which helps us to enhance the components we want to render with multiple graphql().

Observe this:

compose(
  graphql(SAVE_MUTATION, { name: "saveMutation" }),
  graphql(SUBMIT_MUTATION, { name: "submitMutation" })
)(ComposeNest)

compose will give ComposeNest access to the mutations from both graphql().

class ComposeNest extends React.Component {
  state = {
    saveMutationCalled: false,
    dataToSave: "",
    dataToSubmit: null,
    submittedData: null,
  }
  render() {
    const { saveMutation, submitMutation } = this.props
    const { dataToSave, dataToSubmit, submittedData } = this.state
    if (!dataToSubmit) {
      return (
        <>
          <input
            type="text"
            placeholder="Enter your data here"
            value={dataToSave}
            onChange={e => {
              this.setState({
                dataToSave: e.target.value,
              })
            }}
          />
          <button
            onClick={async () => {
              const dataToSubmit = await saveMutation({
                variables: {
                  dataToSave,
                },
              })
              this.setState({
                dataToSubmit,
              })
            }}
          >
            Save
          </button>
        </>
      )
    } else {
      return (
        <>
          <span>Data to submit: {dataToSubmit.data.save} </span>
          {submittedData && <p>{submittedData.data.submit}</p>}
          {!submittedData && (
            <button
              onClick={async () => {
                const submittedData = await submitMutation({
                  variables: {
                    dataToSubmit: dataToSubmit.data.save,
                  },
                })
                this.setState({
                  submittedData,
                })
              }}
            >
              Submit
            </button>
          )}
        </>
      )
    }
  }
}

One great thing about compose and graphql(): no render props!

4) react-apollo-hooks

react-apollo-hooks is not an official Apollo library (the official version is in beta) but it works great.

The package provides us with several hooks: useQuery, useMutation, useSubscription and useApolloClient. In order for them to work, react-apollo-hooks needs you to wrap the components using those hooks with its own ApolloProvider. For example:

import { ApolloProvider } from "react-apollo"
import { ApolloProvider as ApolloHooksProvider } from "react-apollo-hooks"
// other imports

function App() {
  return (
    <ApolloProvider client={client}>
      <ApolloHooksProvider client={client}>
        {/*Components to render*/}
      </ApolloHooksProvider>
    </ApolloProvider>
  )
}

To use the hooks, we need to pass the gql string as the first argument, and the queries and mutations options as the second.

With our scenario in mind, our useMutation might look like the following:

const saveMutation = useMutation(SAVE_MUTATION, {
  variables: {
    dataToSave,
  },
})

const submitMutation = useMutation(SUBMIT_MUTATION, {
  variables: {
    dataToSubmit,
  },
})

The useMutation returns a mutation function that we can invoke anywhere we want.

Here’s the full code which utilises those hooks:

import React, { useState } from "react"
import { useMutation } from "react-apollo-hooks"
import { SAVE_MUTATION, SUBMIT_MUTATION } from "./DefaultNest"

const HooksNest = () => {
  const [state, setState] = useState({
    saveMutationCalled: false,
    dataToSave: "",
    dataToSubmit: null,
    submittedData: null,
  })

  const { saveMutationCalled, dataToSave, dataToSubmit, submittedData } = state

  const saveMutation = useMutation(SAVE_MUTATION, {
    variables: {
      dataToSave,
    },
  })
  const submitMutation = useMutation(SUBMIT_MUTATION, {
    variables: {
      dataToSubmit,
    },
  })
  if (!dataToSubmit) {
    return (
      <>
        <input
          type="text"
          placeholder="Enter your data here"
          value={dataToSave}
          onChange={e => {
            setState({
              ...state,
              dataToSave: e.target.value,
            })
          }}
        />
        <button
          onClick={async () => {
            const dataToSubmit = await saveMutation({
              variables: {
                dataToSave,
              },
            })
            setState({
              ...state,
              dataToSubmit,
            })
          }}
        >
          Save
        </button>
      </>
    )
  } else {
    return (
      <>
        <span>Data to submit: {dataToSubmit.data.save} </span>
        {submittedData && <p>{submittedData.data.submit}</p>}
        {!submittedData && (
          <button
            onClick={async () => {
              const submittedData = await submitMutation({
                variables: {
                  dataToSubmit: dataToSubmit.data.save,
                },
              })
              setState({
                ...state,
                submittedData,
              })
            }}
          >
            Submit
          </button>
        )}
      </>
    )
  }
}

export default HooksNest

Like graphql() and compose, no render props needed in render().

Conclusion

I can’t say for sure which methods are best when dealing with render props. Normal nesting can work if you don’t use multiple Query or Mutation components but it can get messy. Using any of the other three options works well at making your code cleaner.

Here’s a potential caveat: if you use loading and error, I don’t think the graphql and compose combo, and react-apollo-hooks provides you those values.

And a reminder that there’s a repo for all the examples outlined in this post: ways-to-nest-mutation. You can run the frontend and backend with npm start. Just remember to npm install both directories.

Thanks for reading.


Sign up for updates