Seamless integration for GraphQL and React
James Baxley III
One of the things that makes React wonderful is how everything you need to know about a view is all in one place. The logic behind the view, the markup it creates, and even the css is all in one file. This idea is called colocation, and it makes your application much more testable and understandable.
GraphQL, as a declarative query language, brings clarity to the data you are requesting and will receive on a request. When combined, GraphQL and React can give you a full picture of what is going on in a single file.
One of the troubles people experience when joining React and GraphQL is in finding an easy way to convert GraphQL data into the prop shape you want. In many cases, you could end up with a lot of prop reshaping logic in components, or you might need to write your UI components to match the shape of the GraphQL result.
The goal of Apollo is to make working with GraphQL simple and productive, and the react-apollo project tries to do the same for React integration. The new API allows you to describe your data requirements and convert the results into the shape your components need, making it an even better way to connect your app to server-side data!
The Challenge
React components (stateless or class-based) are reusable and testable. The props can be defined (using PropTypes, Typescript, or Flow), and they can be rendered on the server or in a browser, or they can be compiled to native code using react native.
Any integration with GraphQL should keep these benefits while making it clear what data you will receive from a query or change via a mutation. It should be both declarative and located next to the component.
The Solution
The new version of react-apollo solves this with the graphql higher order component. It solves a number of difficulties related to combining GraphQL and React, which I’ll describe below.
Colocation of data:
Keeping the query close to the component allows a developer to know what data they have available in the component, as well as making changes much easier. It reduces side effects (this query is used only by this component) and makes it easy to mock and test individual components.
const Container = graphql(gql`query GetUser { person { firstName } }`)(({ data }) => {
const { loading, person } = data;
if (loading) return <div>Loading...<div>
return <h1>Hello {person.firstName}</h1>
});
Meaningful props:
Given that react components are so reusable, it is a lot easier if you can adjust the shape of your data to fit the needs of the component, instead of making a number of prop options or tedious logic within shared components. This is why React Apollo lets you change the shape of your props to fit the needs of your components by passing a transformer function in the props option to the container:
import React, { Component } from 'react';
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
const GET_USER_WITH_ID = gql`
query getUser(id: $ID!) {
user { name }
}
`;
const withUser = graphql(GET_USER_WITH_ID, {
// `ownProps` are the props passed into `MyComponentWithData`
// `data` is the result data (see above)
props: ({ ownProps, data }) => {
if (data.loading) return { userLoading: true };
if (data.error) return { hasErrors: true };
return {
currentUser: data.user,
refetchUser: data.refetch,
};
}
});
class MyComponent extends Component { ... }
const MyComponentWithData = withUser(MyComponent);
MyComponent.propTypes = {
userLoading: React.PropTypes.boolean,
hasErrors: React.PropTypes.boolean,
currentUser: React.PropTypes.object,
refetchUser: React.PropTypes.func,
};
This is also really helpful for making sure your presentational components aren’t aware of your mutation logic. Using the same props option, you can create custom functions which accept the minimal arguments needed to create a mutation.
For example, look at this mutation from GitHunt, the official Apollo example app:
const SUBMIT_COMMENT_MUTATION = gql`
mutation submitComment($repoFullName: String!, $commentContent: String!) {
submitComment(repoFullName: $repoFullName, commentContent: $commentContent) {
postedBy {
login
html_url
}
createdAt
content
}
}
`;
// used to update the Comment query after the muation runs
const updateQueries = {
Comment: (prev, { mutationResult }) => {
const newComment = mutationResult.data.submitComment;
return update(prev, {
entry: { comments: { $unshift: [newComment] } },
});
},
};
const CommentsPageWithMutations = graphql(SUBMIT_COMMENT_MUTATION, {
props: ({ ownProps, mutate }) ({
submit: ({ repoFullName, commentContent }) => mutate({
variables: { repoFullName, commentContent },
optimisticResponse: {
__typename: 'Mutation',
submitComment: {
__typename: 'Comment',
postedBy: ownProps.currentUser,
createdAt: +new Date,
content: commentContent,
},
},
updateQueries,
});
}),
})(CommentsPage);
The CommentsPage component can now call this.props.submitwith just the name of the repo and the content of the comment. Thanks to the custom function wrapping the mutation, it will have an optimistic response and update query results without the component needing to worry about it.
Easy configuration:
Apollo Client gives a ton of power to client side GraphQL usage. It allows for passing variables, batching, polling, sharing fragments and more. The React integration also allows for skipping a full query and turning off SSR on a per query basis. You can customize these features by passing options to the graphql container. Options can be based on the component’s props, allowing you to get them from anywhere you need, including your Redux state.
Here’s an example of adding polling:
const withPollingQuery = graphql(GET_USER_WITH_ID, {
options: () => ({ pollInterval: 1000 })
});
// alternatively you can pass an object
// const withPollingQuery = graphql(GET_USER_WITH_ID, {
// options: { pollInterval: 1000 }
// });
const MyComponentWithData = withPollingQuery(MyComponent);
Here’s an example of skipping a query:
const withUser = graphql(GET_USER_DATA, {
options: (ownProps) => ({ skip: !ownProps.authenticated })
});
const MyComponentWithData = withUser(MyComponent);
Server-side rendering:
Server side rendering in React is powerful, but is also often hard to configure. If you want to do more than render a lot of loading icons, you need to prefetch all of your data for your entire tree before you render it. This is commonly done by fetching all data at the top of your router component, then long passing that data all the way through the app tree. However, this is inefficient on the client, and hard to keep track of with a larger page / app.
With react-apollo, you can declare your data needs all throughout your react tree. When you get ready to render on the server, run getDataFromTree or renderToStringWithData to fetch all data throughout your tree and render your entire app! This makes your client best practices useable during server rendering; everything stays small and where it belongs.
Your users will thank you for the faster time to action and your site/app will benefit from better SEO support.
Even though you get all these benefits from server-side rendering, setting it up is super easy. This is what is looks like on the server:
import { renderToStringWithData } from "react-apollo/server"
// during request
renderToStringWithData(app).then(markup => {
// send markup to client
});
The method for SSR in react-apollo isn’t hard-coded to this library. Any component with a static fetchData method can return a promise that will block the request until it is resolved. At some point in the future, the SSR methods can become aliases to a generic getDataFromTree method for all React apps and data loading methods.
Testing:
Since data is next to the component, you can mock your network requests and write unit test and integration tests for components easily. This means you can test mutation handling, changing data, and in the near future, reactive data in your component. You can check out the test directory of the react-apollo repo for lots of examples, but here is one to showcase how this can be really easy:
it('executes a query', (done) => {
const query = gql` query people { allPeople(first: 1) { people { name } } }`;
const data = { allPeople: { people: [ { name: 'Luke Skywalker' } ] } };
const networkInterface = mockNetworkInterface({ request: { query }, result: { data } });
const client = new ApolloClient({ networkInterface });
const withGraphQL = graphql(query);
class Container extends Component {
componentWillReceiveProps(props) {
expect(props.data.loading).to.be.false;
expect(props.data.allPeople).to.deep.equal(data.allPeople);
done();
}
render() {
return null;
}
};
const ContainerWithData = withGraphQL(Container);
mount(<ApolloProvider client={client}><ContainerWithData /></ApolloProvider>);
});
The full discussion around the integration can be found on the GitHub repo and the docs can be found here. I hope you get a chance to try it out and find that it makes development easier! Special thanks to Sashko Stubailo, Maxime Quandalle, Tom Coleman, and all who helped with the design and testing!
Finally, I really wanted to make sure that the client worked great on the server, the client, and in react native. Thanks to Christoph Pojer and Jest, tests run in all of these environments on all commits!