Skip to content

Instantly share code, notes, and snippets.

@jbaxleyiii
Created April 16, 2016 01:23
Show Gist options
  • Save jbaxleyiii/2ec2a8efc38d4e32ff9fbce4e148d303 to your computer and use it in GitHub Desktop.
Save jbaxleyiii/2ec2a8efc38d4e32ff9fbce4e148d303 to your computer and use it in GitHub Desktop.
Explore slight API changes to react-apollo

Here are some thoughts on a slight tweak to the query API and the mutation API.

Queries

Currently the API has a watchQuery method that is passed to the user to use in order to create queries. The build a dictionary of [key: string]: WatchQueryHandles that gets mapped to the props passed to the wrapped component. The adjustments below have the user creating a dictionary of [key: string]: WatchQueryOptions. The reason for doing this is two fold. 1st the connect component decides how to get data from the store. Behind the scenes it will be calling watchQuery but in the future if we wanted to add / make changes behind the scenes we could call different methods for different data (not sure if useful). The real benefit in my option is the WatchQueryOptions. Since the watchQuery handle isn't called when getting the dictionary anymore, we can get the dictionary in componentWillMount, use the WatchQueryOptions to see if we already have all of the data using getFromStore methods and if we do, the intial state passed to the wrapped component would be the full data + no errors + not loading. Then we could bind the watchQuery on componentDidMount for reactivity purposes. If we don't have the data, we can easily pass default data { loading: false, error: null, result: null } on componentWillMount and create the query on componentDidMount. Doing this also allows us to extend and when rendering on the server, we can call client.query(WatchQueryOptions) and use the resulting promise to delay SSR until the data has returned and prefill the store.

Mutations

Similar to the changes in query, mutations return a dictionary of [key: string]: MutationOptions which is used to bind the resulting props. The prop of each key represents a custom function (with as many args as wanted) which calls client.mutate(options) behind the scenes. The key on the props also represents the state of props in the same shape as the query props ({ loading: false, error: null, result: null }). We could also add a hasBeenCalled: boolean or some other key if wanted but I think between loading, error, and result, a dev should be able to reason that if needed. This gets us the same benefits on the SSR side so something like an analytics mutation component could report SSR page loads or other data if desired. I think the mixed method + object is a little odd but it seems to be the cleanest solution overall

Added props

Simillar to redux's @connect, I think that our's should pass dispatch, query, and mutate as props to the wrapped component. That way if custom client actions (one off mutations or queries) are wanted, they are possible. It also means @connect() is a non reactive way to get access to the applications client API.

import { connect } from 'apollo-react';

function mapQueriesToProps({ ownProps, state }) {
  return {
    category: {
      query: `
        query getCategory($categoryId: Int!) {
          category(id: $categoryId) {
            name
            color
          }
        }
      `,
      variables: {
        categoryId: 5,
      },
      forceFetch: false,
      returnPartialData: true,
    }
  }
}

function mapMutationsToProps({ ownProps, state }) {
  return {
    addCategory(/* args */) {
      return {
        mutation: `
          mutation postReply(
            $topic_id: ID!
            $category_id: ID!
            $raw: String!
          ) {
            createPost(
              topic_id: $topic_id
              category: $category_id
              raw: $raw
            ) {
              id
              cooked
            }
          }
        `,
        variables: {
          // Use the container component's props
          topic_id: ownProps.topic_id,

          // Use the redux state
          category_id: state.selectedCategory,

          // Use an argument passed from the callback
          /* args */,
        }
      };
    },
    otherAction(controlArg1, controlArg2) {
      let mutation = `
          mutation postReply(
            $topic_id: ID!
            $category_id: ID!
            $raw: String!
          ) {
            createPost(
              topic_id: $topic_id
              category: $category_id
              raw: $raw
            ) {
              id
              cooked
            }
          }
        `
      if (controlArg1) {
        mutation = `
          different mutation
        `
      }
      
      return {
        mutation,
        variables: {
          // Use the container component's props
          topic_id: ownProps.topic_id,

          // Use the redux state
          category_id: state.selectedCategory,

          // Use an argument passed from the callback
          /* args */,
        }
      };
    },
  }
}

@connect({ mapQueriesToProps, mapMutationsToProps })
class AddCategory extends Component {
 
  onAddCategoryClick(e) {
    const { target } = e;
    const { addCategory } = this.props;
    
    this.props.addCategory(target.id, target.value);
  }
  
  onOtherAction(e) {
    const { target } = e;
    const { otherAction } = this.props;
    
    this.props.otherAction(target.id, target.value);
  }
  
  onCustomComponentMutation(mutation, variables) {
    
    this.props.mutate({ mutation, variables })
      .then((graphQLResult) => {
        const { errors, data } = graphQLResult;

        if (data) {
          console.log('got data', data);
        }

        if (errors) {
          console.log('got some GraphQL execution errors', errors);
        }
      }).catch((error) => {
        console.log('there was an error sending the query', error);
      });
    
  }
  
  onCustomQuery(query, variables) {
    this.props.query({ query, variables })
      .then((graphQLResult) => {
        const { errors, data } = graphQLResult;

        if (data) {
          console.log('got data', data);
        }

        if (errors) {
          console.log('got some GraphQL execution errors', errors);
        }
      }).catch((error) => {
        console.log('there was an error sending the query', error);
      });
  }
  
  render() {
    
    /*
    
      this.props.addCategory is a function that calls a mutation and has the added data of:
      
      {
        loading: boolean,
        error: Error,
        result: GraphQLResult,
      }
      
      this.props.categories is a just an object representing the query:
      
      {
        loading: boolean,
        error: Error,
        result: GraphQLResult,
      }
      
    */

  }
  
}

Thoughs?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment