In this section, we will create a simple app that demonstrates routing with WPGraphQL's nodeByUri
query. We will use this query to fetch data for a shop page that displays a list of products with their "name", "shortDescription", "price", and "image". The shop page will use the uri parameter to fetch the data and render the page accordingly.
- Basic knowledge of React and React Router.
- Familiarity with GraphQL and WPGraphQL.
- A setup WPGraphQL/WooGraphQL backend.
Let's start by setting up a simple React application with React Router. We will create a basic shop page that fetches the products data based on the uri
.
import React from 'react'; import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; import ShopPage from './ShopPage'; function App() { return ( <Router> <Switch> <Route path="/shop" component={ShopPage} /> </Switch> </Router> ); } export default App;
In the ShopPage
component, we will use the nodeByUri
query to fetch the data for the shop page. Based on the data received, we will render a product listing for either a collection or a single data object. We'll start by creating the graphql.js
file.
import { gql } from '@apollo/client'; export const NodeByUri = gql` query NodeByUri($uri: ID!) { nodeByUri(uri: $uri) { ... on Product { id name shortDescription price image { sourceUrl altText } } contentNodes(first: 100) { edges { cursor node { ... on Product { id name shortDescription ... on SimpleProduct { price } ... on VariableProduct { price } image { sourceUrl altText } } } } pageInfo { hasNextPage endCursor } } } } `;
import React from 'react'; import { useQuery } from '@apollo/client'; import ProductListing from './ProductListing'; // Import the NodeByUri query here import { NodeByUri } from './graphql'; const ShopPage = () => { const { loading, error, data } = useQuery(NodeByUri, { variables: { uri: '/shop' }, }); if (loading) return <p>Loading...</p>; if (error) return <p>Error: {error.message}</p>; const products = data?.nodeByUri?.contentNodes?.edges?.map( ({ node }) => node ) || []; return <ProductListing products={products} />; }; export default ShopPage;
The ShopPage
component fetches the data using the nodeByUri
query and updates the state with the received data. It then passes the products data to the ProductListing
component for rendering.
The ProductListing
component takes the products data and renders a list of products.
Notice that we are not checking to see if the fields that are nullable aren't empty values before rendering them. You should always do so, but we skip it here for readability.
import React from 'react'; const ProductListing = ({ products }) => { return ( <div> <h2>Shop</h2> <ul> {products.map((product) => ( <li key={product.id}> <h3>{product.name}</h3> <p>{product.shortDescription}</p> <p>Price: {product.price}</p> <img src={product.image.sourceUrl} alt={product.image.altText} /> </li> ))} </ul> </div> ); }; export default ProductListing;
With the ProductListing
component, we can display the product listing for both collection and single data object. This approach can also be applied to other pages such as /product-category/*
or /product-tag/*
pages, with the ability to change there slug names as well in the WP Dashboard.
In the next section, we will focus further on rendering a product listing using the nodeByUri
query by exploring adding features like pagination, sorting, and filtering to our shop page.
To add pagination to our shop page, we will need to create a type policy for our schema. This will tell Apollo how to cache our query results.
import { ApolloClient, from } from '@apollo/client'; import { relayStylePagination } from '@apollo/client/utilities'; const typePolicies = { RootQuery: { queryType: true, fields: { products: relayStylePagination(['where']), }, }, }; const client = new ApolloClient({ link: from([ // ...middleware/afterware/endpoint ]), cache: new InMemoryCache({ typePolicies }), });
relayStylePagination()
is a utility function that merges to the results of a Relay Connection together and as of the writing of this documentation has a slight bug where it only merges the edges
and not the nodes
, if your wondering why we're using edges
instead of nodes
.
Next we have to update the NodeByUri
operation to include the after
and first
variables. This will allow us to fetch a specific number of products and control the starting point for the fetched data.
Notice we are not applying the first
and after
variables to the query but instead a connection within.
First, update the NodeByUri
query in graphql.js
:
query NodeByUri($uri: ID!, $first: Int, $after: String) { nodeByUri(uri: $uri) { ... on Product { id name shortDescription price image { sourceUrl altText } } contentNodes(first: $first, after: $after) { edges { cursor node { ... on Product { id name shortDescription ... on SimpleProduct { price } ... on VariableProduct { price } image { sourceUrl altText } } } } pageInfo { hasNextPage endCursor } } } }
Next, update the ShopPage
component to manage the pagination state and fetch more products using the fetchMore
function from the useQuery
hook:
import React from 'react'; import { useQuery } from '@apollo/client'; import ProductListing from './ProductListing'; // Import the NodeByUri query here import { NodeByUri } from './graphql'; const ShopPage = () => { const { loading, error, data, fetchMore } = useQuery(NodeByUri, { variables: { uri: '/shop', first: 10 }, }); const loadMoreProducts = () => { if (data.nodeByUri.contentNodes.pageInfo.hasNextPage) { fetchMore({ variables: { after: data.nodeByUri.contentNodes.pageInfo.endCursor, }, }); } }; if (loading) return <p>Loading...</p>; if (error) return <p>Error: {error.message}</p>; const products = data?.nodeByUri?.contentNodes?.edges?.map( ({ node }) => node ) || []; return ( <> <ProductListing products={products} /> <button onClick={loadMoreProducts}>Load More</button> </> ); }; export default ShopPage;
Now, when you click the "Load More" button, it will fetch more products and add them to the list.
To add sorting functionality, we will update the NodeByUri
query again to include a where
argument in the contentNodes
field. This will allow us to control the order in which the products are fetched.
Update the NodeByUri
query in graphql.js
:
query NodeByUri($uri: ID!, $first: Int, $after: String, $where: RootQueryToProductConnectionWhereArgs) { nodeByUri(uri: $uri) { ... on Product { id name shortDescription price image { sourceUrl altText } } contentNodes(first: $first, after: $after, where: $where) { edges { nodes { ... on Product { id name shortDescription ... on SimpleProduct { price } ... on VariableProduct { price } image { sourceUrl altText } } } } pageInfo { hasNextPage endCursor } } } }
Next, add a sorting dropdown component to the ShopPage
component, and update the state and the useQuery
hook to handle sorting:
import React from 'react'; import { useQuery } from '@apollo/client'; import ProductListing from './ProductListing'; // Import the NodeByUri query here import { NodeByUri } from './graphql'; const ShopPage = () => { const [sort, setSort] = useState(null); const { loading, error, data, fetchMore } = useQuery(NodeByUri, { variables: { uri: '/shop', first: 10, where: sort }, }); const loadMoreProducts = () => { if (data.nodeByUri.contentNodes.pageInfo.hasNextPage) { fetchMore({ variables: { after: data.nodeByUri.contentNodes.pageInfo.endCursor, }, }); } }; const handleSortChange = (e) => { setSort({ orderby: e.target.value }); }; if (loading) return <p>Loading...</p>; if (error) return <p>Error: {error.message}</p>; const products = data?.nodeByUri?.contentNodes?.edges?.map( ({ node }) => node ) || []; return ( <> <select onChange={handleSortChange}> <option value="DATE">Newest</option> <option value="PRICE_ASC">Price: Low to High</option> <option value="PRICE_DESC">Price: High to Low</option> </select> <ProductListing products={products} /> <button onClick={loadMoreProducts}>Load More</button> </> ); }; export default ShopPage;
Now, when you change the sorting option in the dropdown, the products will be fetched and displayed in the selected order.
To add filtering functionality, you will need to make further updates to the NodeByUri
query and the ShopPage
component. In this example, we will add a simple search input to filter products based on their names.
Update the NodeByUri
query in graphql.js
to include a search argument in the where field:
query NodeByUri($uri: ID!, $first: Int, $after: String, $where: RootQueryToProductConnectionWhereArgs) { nodeByUri(uri: $uri) { ... on Product { id name shortDescription price image { sourceUrl altText } } ... on Collection { contentNodes(first: $first, after: $after, where: $where) { edges { nodes { ... on Product { id name shortDescription ... on SimpleProduct { price } ... on VariableProduct { price } image { sourceUrl altText } } } pageInfo { hasNextPage endCursor } } } } } }
Next, add a search input component to the ShopPage
component, and update the state and the useQuery
hook to handle filtering:
import React from 'react'; import { useQuery } from '@apollo/client'; import ProductListing from './ProductListing'; // Import the NodeByUri query here import { NodeByUri } from './graphql'; const ShopPage = () => { const [sort, setSort] = useState(null); const [search, setSearch] = useState(''); const { loading, error, data, fetchMore } = useQuery(NodeByUri, { variables: { uri: '/shop', first: 10, where: { ...sort, search } }, }); const loadMoreProducts = () => { if (data.nodeByUri.contentNodes.pageInfo.hasNextPage) { fetchMore({ variables: { after: data.nodeByUri.contentNodes.pageInfo.endCursor, }, }); } }; const handleSortChange = (e) => { setSort({ orderby: e.target.value }); }; const handleSearchChange = (e) => { setSearch(e.target.value); }; if (loading) return <p>Loading...</p>; if (error) return <p>Error: {error.message}</p>; const products = data?.nodeByUri?.contentNodes?.edges?.map( ({ node }) => node ) || []; return ( <> <input type="text" placeholder="Search products..." value={search} onChange={handleSearchChange} /> <select onChange={handleSortChange}> <option value="DATE">Newest</option> <option value="PRICE_ASC">Price: Low to High</option> <option value="PRICE_DESC">Price: High to Low</option> </select> <ProductListing products={products} /> <button onClick={loadMoreProducts}>Load More</button> </> ); }; export default ShopPage;
With these changes, you can now search for products by typing in the search input, and the products will be fetched and displayed based on the search query. This is far from complete. It needs many more things, like CSS styling, field validation, and error handling to name a few.
In summary, you've now implemented sorting and filtering functionality for products in a headless WordPress + React app using WooGraphQL. You can further customize the sorting and filtering options by modifying the NodeByUri
query and the ShopPage
component.