This post was originally published on jkettmann.com
Apollo's Query
and Mutation
components are easy to use. But since they use the render prop pattern they often decrease readability. This is especially true when you need to nest them, for example when a component needs to both query and mutate data.
Another problem is that you often end up with a container component responsible for fetching data which wraps a component that includes business logic. One example is when you want to use a hook that depends on the fetched data like below.
<Query query={SOME_QUERY}>
{({ data }) => {
const transformedData = useMemo(() => transform(data));
return <div>...</div>;
}}
</Query>
Using a hook here is not possible, so we need to extract the inner component. Now we have one component which only renders the Query
and a second one that renders the data coming from the query. This demolishes one of the great benefits of GraphQL and Apollo: defining the data requirements next to the rendering of that data.
But finally, we have a better way to solve this kind of problems. With the new release of Apollo's version, 3 hooks are supported! This is a great step forward. The Apollo team obviously is also excited since they rewrote their documentation with hooks.
Since I ran into a couple of small problems when first using them I'd like to provide others a small guide on how to migrate to Apollo hooks.
Of course, it's best to see them in action. So let's start with a simple React application that contains a Query
and a Mutation
component.
First, we simply initialize Apollo using apollo-boost
.
import React from "react";
import ReactDOM from "react-dom";
import ApolloClient from "apollo-boost";
import { ApolloProvider } from "react-apollo";
import "./index.css";
import App from "./App";
const client = new ApolloClient({
uri: "http://localhost:4000/graphql"
});
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById("root")
);
Next, we define the App component. It contains a BOOKS_QUERY
which asks for a list of books and an ADD_BOOK_MUTATION
which adds a book to that list.
The books are then rendered. The mutation is called when a button is clicked. It will add Stephen Kings' "The Shining" to the list of books and refetch the BOOKS_QUERY
.
import React from "react";
import { Query, Mutation } from "react-apollo";
import gql from "graphql-tag";
const BOOKS_QUERY = gql`
query books {
books {
id
title
author
}
}
`;
const ADD_BOOK_MUTATION = gql`
mutation addBook($title: String!, $author: String!) {
addBook(title: $title, author: $author) {
id
title
author
}
}
`;
function App() {
return (
<Query query={BOOKS_QUERY}>
{({ loading, error, data }) => {
if (loading) return <div>Loading</div>;
if (error) return <div>Error: {JSON.stringify(error)}</div>;
return (
<div>
{data.books.map(({ id, title, author }) => (
<div key={id}>
"{title}" by "{author}"
</div>
))}
<Mutation
mutation={ADD_BOOK_MUTATION}
variables={{
title: 'The Shining',
author: 'Steven King'
}}
refetchQueries={[{ query: BOOKS_QUERY }]}
>
{addBook => <button onClick={addBook}>Add book</button>}
</Mutation>
</div>
);
}}
</Query>
);
}
export default App;
Now this doesn't look very beautiful, does it? For example, we have 11 indentations inside the button component. We could, of course, extract smaller components. At the same time, it doesn't feel like the component should appear this complicated.
So let's see how it will look like once we migrate to Apollo hooks.
With Apollo's version 3 three packages have been introduced to separate the higher-order components (@apollo/react-hoc
), render prop components (@apollo/react-components
) and hooks (@apollo/react-hooks
). This allows us to have smaller bundle sizes. The hooks package is the smallest in size since the others depend on it.
The original react-apollo
serves as an umbrella package which allows us to use all patterns in parallel.
As the first step of our migration, we need to install new dependencies. We will simulate a gradual migration to hooks like you would do with a bigger real-live application. This means we will only replace the Query
component by the useQuery
hook in the first step and still use the old Mutation
component in parallel. Thus we need to upgrade the react-apollo
package as well.
npm i @apollo/react-hooks react-apollo@3
We can now replace the Query
component by the useQuery
hook. This way we can move all the query logic up before we return the JSX.
import React from 'react';
import { Mutation } from 'react-apollo';
import { useQuery } from '@apollo/react-hooks';
import gql from 'graphql-tag';
const BOOKS_QUERY = ...;
const ADD_BOOK_MUTATION = ...;
function App() {
const { loading, error, data } = useQuery(BOOKS_QUERY);
if (loading) return <div>Loading</div>;
if (error) return <div>Error: {JSON.stringify(error)}</div>;
return (
<div>
{
data.books.map(({ id, title, author }) => (
<div key={id}>
"{title}" by "{author}"
</div>
))
}
<Mutation
mutation={ADD_BOOK_MUTATION}
variables={{
title: 'The Shining',
author: 'Steven King',
}}
refetchQueries={[{ query: BOOKS_QUERY }]}
>
{
(addBook) => (
<button onClick={addBook}>
Add book
</button>
)
}
</Mutation>
</div>
);
}
export default App;
This looks already much nicer. We didn't need to change much but we already got rid of four indentations. Additionally, the ugly conditionals nested inside the JSX code are gone. Great improvement in readability! And good news: The app still works even though we only partially migrated to hooks.
Now we can also replace the Mutation
component by the useMutation
hook.
import React from 'react';
import { useQuery, useMutation } from '@apollo/react-hooks';
import gql from 'graphql-tag';
const BOOKS_QUERY = ...;
const ADD_BOOK_MUTATION = ...;
function App() {
const { loading, error, data } = useQuery(BOOKS_QUERY);
const [addBook] = useMutation(ADD_BOOK_MUTATION, {
variables: {
title: 'The Shining',
author: 'Steven King',
},
refetchQueries: [{ query: BOOKS_QUERY }],
});
if (loading) return <div>Loading</div>;
if (error) return <div>Error: {JSON.stringify(error)}</div>;
return (
<div>
{
data.books.map(({ id, title, author }) => (
<div key={id}>
"{title}" by "{author}"
</div>
))
}
<button onClick={addBook}>
Add book
</button>
</div>
);
}
export default App;
This looks really clean! We have a component which looks simple but actually does a lot. It fetches data from a server, renders it and is also able to mutate that data.
What I like most is the clean separation of concerns within the component. In the upper part of the component, we handle the data. Next comes the conditional rendering of the loading and error state. Last we render the actual component.
Last but not least we can also improve our bundle size by removing the react-apollo
package from the dependencies. Now we only need to import ApolloProvider
from the hooks package in our entry file.
import React from "react";
import ReactDOM from "react-dom";
import ApolloClient from "apollo-boost";
import { ApolloProvider } from "@apollo/react-hooks";
import "./index.css";
import App from "./App";
const client = new ApolloClient({
uri: "http://localhost:4000/graphql"
});
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById("root")
);
If you liked this post I'd be happy to see you on my newsletter or Twitter.
Top comments (0)