GraphQL helps empower the front-end developer to be able to retrieve data from a single endpoint with ease. By utilizing the query function, we can receive data stored remotely through a JSON-like structure. Since GraphQL prioritizes speed and ease of setup, there are some features that you will need to pass to your GraphQL client in order to use certain attributes in your query. One of these features is fragments. Fragments are a very powerful and useful feature in GraphQL, and if you do not know what fragments are you can read more here. While fragments can be powerful, there is a common mistake made by many front-end developers when it comes to handling queries with unions and interfaces: They are using the simple (heuristic) fragment matcher. However, the fix is quite simple and below details step by step how to fix this problem.
The Problem
Take for example a simple e-commerce web application that uses Magento as your e-commerce platform and the front-end is written using React.
So, let’s say you write a cart component that uses the following query to fetch all the data in your cart:
query cartDetails($cartId: String!) { cart(cart_id: $cartId) { id total_quantity items { id sku quantity prices { row_total { value } } product { name thumbnail { url } } ... on ConfigurableCartItem { configurable_options { value_label option_label } } } prices { grand_total { value } } } }
On lines 19-24 you will notice an interface fragment used that is built into Magento’s GraphQL implementation to allow for more information from a cart item if that cart item is a Configurable Item.
Let’s go ahead and jump over to the browser and add a Configurable Item to the cart and see what this query returns to us:
Awesome! Everything is working as expected, so let’s move on and add a simple item to the cart.
Everything looks fine at first but if you look closely, you can see the configurable options that we selected are now missing on the Configurable Cart Item. Why is this happening? Well if we go to the console and we are in the development environment we see an error. The error tells you that you are “using the simple (heuristic) fragment matcher but our query contains a union or interface type…”. Although this is not breaking the code entirely, this is not the expected behavior. Luckily there is an easy, automated solution for this problem.
The Solution
Let’s back up a little and approach this by writing our own wrapper component for GraphQL that we can simply import into our app to provide access to the GraphQL universe from within our components. With that being said, we are going to be using the Apollo-Boost package from Apollo to quickly setup our connection to Magento’s GraphQL endpoint.
First thing to do is set up a very basic GraphQL wrapper component:
import React from "react"; import { ApolloProvider } from "@apollo/react-hooks"; const GraphQLWrapper = ({ uri = process.env.REACT_APP_GRAPHQL_URI, children, }) => { const Store = document.querySelector("body").dataset.storeView || "default"; const client = new ApolloClient({ uri, headers: { Store }, }); return <ApolloProvider client={client}>{children}</ApolloProvider>; }; export default GraphQLWrapper;
This component can be imported into any React component and wrapped around it to be able to use the @apollo/react-hooks
hooks that are included in this package (such as useQuery & useMutation).
Next, we need to create a new file that will do some work for us to grab the schema from Magento’s GraphQL endpoint:
const fetch = require("node-fetch"); const fs = require("fs"); const path = require("path"); process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; fetch(process.env.REACT_APP_GRAPHQL_URI, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ variables: {}, query: `{ __schema { types { kind name possibleTypes { name } } } }`, }), }) .then(result => result.json()) .then(result => { result.data.__schema.types = result.data.__schema.types.filter( type => type.possibleTypes !== null ); fs.writeFileSync( path.resolve(__dirname, "./fragments/fragmentTypes.json"), JSON.stringify(result.data), err => { if (err) { console.error("Error writing fragmentTypes file", err); } else { console.log("Fragment types successfully extracted!"); } } ); });
This file will reach out to the GraphQL endpoint and ask for the schema of the GraphQL server and write that result into a JSON file and store it locally on your machine. My recommendation is to have git ignore the fragmentTypes.json
file, as we do not need to have version control on this file. Now we need a way to run this file before every build so it can be included in it and so we know we are getting the latest schema every time.
If you are using Webpack and more specifically a Webpack setup similar to the Create-React-App setup, then you can follow along and add this to the build script (and even to your watch script). Now inside your build (or watch) script you want to add the following lines just after your other imports:
const exec = require("child_process").execSync; // Using the synchronous version to make sure the file is added before the compiler starts try { exec("node src/path/to/schema/file.js"); console.log("GraphQL Fragment Matcher Updated"); } catch (err) { console.error(err); }
Now every time you run the build, the fragmentTypes.json
file will be updated to the latest version. However, we still need to tell the Apollo client to use our fragment matcher and to cache it for every call in the future.
So navigating back to our GraphQLWrapper component, we will update the file to look something like this:
import React from "react"; import ApolloClient, { InMemoryCache, IntrospectionFragmentMatcher, } from "apollo-boost"; import { ApolloProvider } from "@apollo/react-hooks"; import introspectionQueryResultData from "./fragments/fragmentTypes.json"; const GraphQLWrapper = ({ uri = process.env.REACT_APP_GRAPHQL_URI, children, }) => { const fragmentMatcher = new IntrospectionFragmentMatcher({ introspectionQueryResultData, }); const Store = document.querySelector("body").dataset.storeView || "default"; const cache = new InMemoryCache({ fragmentMatcher }); const client = new ApolloClient({ uri, cache, headers: { Store }, }); return <ApolloProvider client={client}>{children}</ApolloProvider>; }; export default GraphQLWrapper;
Finally, we now can navigate back to our cart and refresh to see if this solves our problem:
and it did! There we have it, a simple reusable way of adding an Introspection Fragment Matcher to your GraphQL client that will be automatically updated with every build.
Leave a Reply